This project is sponsored by the software consulting firm ShakaCode, creator of the React on Rails Gem. We focus on React (with TS or ReScript) front-ends, often with Ruby on Rails or Gatsby. See our recent work and client engagement model. Feel free to engage in discussions around this gem at our Slack Channel or our forum category for Cypress.
Interested in joining a small team that loves open source? Check our careers page.
Need help with cypress-on-rails? Contact ShakaCode.
Suggest you first learn the basics of Cypress before attempting to integrate with Ruby on Rails
Suggest you first learn the basics of Playwright before attempting to integrate with Ruby on Rails
Gem for using cypress.io or playwright.dev in Rails and Ruby Rack applications with the goal of controlling state as mentioned in Cypress Best Practices
It allows you to run code in the application context when executing cypress or playwright tests. Do things like:
- use database_cleaner before each test
- seed the database with default data for each test
- use factory_bot to setup data
- create scenario files used for specific tests
Has examples of setting up state with:
- factory_bot
- rails test fixtures
- scenarios
- custom commands
Add this to your Gemfile:
group :test, :development do
gem 'cypress-on-rails', '~> 1.0'
endGenerate the boilerplate code using:
# by default installs only cypress
bin/rails g cypress_on_rails:install
# if you have/want a different cypress folder (default is e2e)
bin/rails g cypress_on_rails:install --install_folder=spec/cypress
# to install playwright instead of cypress
bin/rails g cypress_on_rails:install --framework playwright
# if you target the Rails server with a path prefix to your URL
bin/rails g cypress_on_rails:install --api_prefix=/api
# if you want to install with npm instead
bin/rails g cypress_on_rails:install --install_with=npm
# if you already have cypress installed globally
bin/rails g cypress_on_rails:install --install_with=skip
# to update the generated files run
bin/rails g cypress_on_rails:install --install_with=skipThe generator modifies/adds the following files/directory in your application:
config/initializers/cypress_on_rails.rbused to configure Cypress on Railse2e/cypress/integration/contains your cypress testse2e/cypress/support/on-rails.jscontains Cypress on Rails support codee2e/cypress/e2e_helper.rbcontains helper code to require libraries like factory_bote2e/cypress/app_commands/contains your scenario definitionse2e/playwright/e2e/contains your playwright testse2e/playwright/support/on-rails.jscontains Playwright on Rails support code
If you are not using database_cleaner look at e2e/cypress/app_commands/clean.rb.
If you are not using factory_bot look at e2e/cypress/app_commands/factory_bot.rb.
Now you can create scenarios and commands that are plain Ruby files that get loaded through middleware, the ruby sky is your limit.
When writing and running tests on your local computer it's recommended to start your server in development mode so that changes you
make are picked up without having to restart your local server.
It's recommended you update your database.yml to check if the CYPRESS environment variable is set and switch it to the test database
otherwise cypress will keep clearing your development database.
For example:
development:
<<: *default
database: <%= ENV['CYPRESS'] ? 'my_db_test' : 'my_db_development' %>
test:
<<: *default
database: my_db_testWARNING!!: cypress-on-rails can execute arbitrary ruby code Please use with extra caution if starting your local server on 0.0.0.0 or running the gem on a hosted server
Getting started on your local environment
# start rails
CYPRESS=1 bin/rails server -p 5017
# in separate window start cypress
yarn cypress open --project ./e2e
# or for npm
npx cypress open --project ./e2e
# or for playwright
yarn playwright test --ui
# or using npm
npx playwright test --uiHow to run cypress on CI
# setup rails and start server in background
# ...
yarn run cypress run --project ./e2e
# or for npm
npx cypress run --project ./e2eYou can run your factory_bot directly as well
// spec/cypress/e2e/simple.cy.js
describe('My First Test', () => {
it('visit root', () => {
// This calls to the backend to prepare the application state
cy.appFactories([
['create_list', 'post', 10],
['create', 'post', {title: 'Hello World'} ],
['create', 'post', 'with_comments', {title: 'Factory_bot Traits here'} ] // use traits
])
// Visit the application under test
cy.visit('/')
cy.contains('Hello World')
// Accessing result
cy.appFactories([['create', 'invoice', { paid: false }]]).then((records) => {
cy.visit(`/invoices/${records[0].id}`);
});
})
})You can check the association docs on more ways to setup association with the correct data.
In some cases, using static Cypress fixtures may not provide sufficient flexibility when mocking HTTP response bodies. It's possible to use FactoryBot.build to generate Ruby hashes that can then be used as mock JSON responses:
FactoryBot.define do
factory :some_web_response, class: Hash do
initialize_with { attributes.deep_stringify_keys }
id { 123 }
name { 'Mr Blobby' }
occupation { 'Evil pink clown' }
end
end
FactoryBot.build(:some_web_response => { 'id' => 123, 'name' => 'Mr Blobby', 'occupation' => 'Evil pink clown' })This can then be combined with Cypress mocks:
describe('My First Test', () => {
it('visit root', () => {
// This calls to the backend to generate the mocked response
cy.appFactories([
['build', 'some_web_response', { name: 'Baby Blobby' }]
]).then(([responseBody]) => {
cy.intercept('http://some-external-url.com/endpoint', {
body: responseBody
});
// Visit the application under test
cy.visit('/')
})
cy.contains('Hello World')
})
})# spec/e2e/app_commands/activerecord_fixtures.rb
require "active_record/fixtures"
fixtures_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
fixture_files = Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] }
logger.debug "loading fixtures: { dir: #{fixtures_dir}, files: #{fixture_files} }"
ActiveRecord::FixtureSet.reset_cache
ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)// spec/cypress/e2e/simple.cy.js
describe('My First Test', () => {
it('visit root', () => {
// This calls to the backend to prepare the application state
cy.appFixtures()
// Visit the application under test
cy.visit('/')
cy.contains('Hello World')
})
})Scenarios are named before blocks that you can reference in your test.
You define a scenario in the spec/e2e/app_commands/scenarios directory:
# spec/cypress/app_commands/scenarios/basic.rb
Profile.create name: "Cypress Hill"
# or if you have factory_bot enabled in your cypress_helper
CypressOnRails::SmartFactoryWrapper.create(:profile, name: "Cypress Hill")Then reference the scenario in your test:
// spec/cypress/e2e/scenario_example.cy.js
describe('My First Test', () => {
it('visit root', () => {
// This calls to the backend to prepare the application state
cy.appScenario('basic')
cy.visit('/profiles')
cy.contains('Cypress Hill')
})
})Create a Ruby file in the spec/e2e/app_commands directory:
# spec/e2e/app_commands/load_seed.rb
load "#{Rails.root}/db/seeds.rb"Then reference the command in your test with cy.app('load_seed'):
// spec/cypress/e2e/simple.cy.js
describe('My First Test', () => {
beforeEach(() => { cy.app('load_seed') })
it('visit root', () => {
cy.visit('/')
cy.contains("Seeds")
})
})Scenarios are named before blocks that you can reference in your test.
You define a scenario in the spec/e2e/app_commands/scenarios directory:
# spec/e2e/app_commands/scenarios/basic.rb
Profile.create name: "Cypress Hill"
# or if you have factory_bot enabled in your cypress_helper
CypressOnRails::SmartFactoryWrapper.create(:profile, name: "Cypress Hill")Then reference the scenario in your test:
// spec/playwright/e2e/scenario_example.spec.js
import { test, expect } from "@playwright/test";
import { app, appScenario } from '../../support/on-rails';
test.describe("Rails using scenarios examples", () => {
test.beforeEach(async ({ page }) => {
await app('clean');
});
test("setup basic scenario", async ({ page }) => {
await appScenario('basic');
await page.goto("/");
});
});Please test and give feedback.
Add the npm package:
yarn add cypress-on-rails --dev
This only works when you start the Rails server with a single worker and single thread
Add your VCR configuration to your cypress_helper.rb
require 'vcr'
VCR.configure do |config|
config.hook_into :webmock
endAdd to your cypress/support/index.js:
import 'cypress-on-rails/support/index'Add to your cypress/app_commands/clean.rb:
VCR.eject_cassette # make sure we no cassettes inserted before the next test starts
VCR.turn_off!
WebMock.disable! if defined?(WebMock)Add to your config/cypress_on_rails.rb:
c.use_vcr_middleware = !Rails.env.production? && ENV['CYPRESS'].present?You have vcr_insert_cassette and vcr_eject_cassette available. https://www.rubydoc.info/github/vcr/vcr/VCR:insert_cassette
describe('My First Test', () => {
beforeEach(() => { cy.app('load_seed') })
it('visit root', () => {
cy.app('clean') // have a look at e2e/app_commands/clean.rb
cy.vcr_insert_cassette('cats', { record: "new_episodes" })
cy.visit('/using_vcr/index')
cy.get('a').contains('Cats').click()
cy.contains('Wikipedia has a recording of a cat meowing, because why not?')
cy.vcr_eject_cassette()
cy.vcr_insert_cassette('cats')
cy.visit('/using_vcr/record_cats')
cy.contains('Wikipedia has a recording of a cat meowing, because why not?')
})
})You may perform any custom action before running a CypressOnRails command, such as authentication, or sending metrics. Please set before_request as part of the CypressOnRails configuration.
You should get familiar with Rack middlewares.
If your function returns a [status, header, body] response, CypressOnRails will halt, and your command will not be executed. To execute the command, before_request should return nil.
CypressOnRails.configure do |c|
# ...
# Refer to https://www.rubydoc.info/gems/rack/Rack/Request for the `request` argument.
c.before_request = lambda { |request|
body = JSON.parse(request.body.string)
if body['cypress_token'] != ENV.fetch('SWEEP_CYPRESS_SECRET_TOKEN')
# You may also use warden for authentication:
# if !request.env['warden'].authenticate(:secret_key)
return [401, {}, ['unauthorized']]
end
}
end CypressOnRails.configure do |c|
# ...
# Refer to https://www.rubydoc.info/gems/rack/Rack/Request for the `request` argument.
c.before_request = lambda { |request|
statsd = Datadog::Statsd.new('localhost', 8125)
statsd.increment('cypress_on_rails.requests')
}
endAdd CypressOnRails to your config.ru
# an example config.ru
require File.expand_path('my_app', File.dirname(__FILE__))
require 'cypress_on_rails/middleware'
CypressOnRails.configure do |c|
c.cypress_folder = File.expand_path("#{__dir__}/test/cypress")
end
use CypressOnRails::Middleware
run MyAppadd the following file to Cypress
// test/cypress/support/on-rails.js
// CypressOnRails: don't remove these commands
Cypress.Commands.add('appCommands', (body) => {
cy.request({
method: 'POST',
url: '/__cypress__/command',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
log: true,
failOnStatusCode: true
})
});
Cypress.Commands.add('app', (name, command_options) => {
cy.appCommands({name: name, options: command_options})
});
Cypress.Commands.add('appScenario', (name) => {
cy.app('scenarios/' + name)
});
Cypress.Commands.add('appFactories', (options) => {
cy.app('factory_bot', options)
});
// CypressOnRails: end
// The next is optional
beforeEach(() => {
cy.app('clean') // have a look at cypress/app_commands/clean.rb
});If your Rails server is exposed under a proxy, typically https://my-local.dev/api, you can use the api_prefix option.
In config/initializers/cypress_on_rails.rb, add this line:
CypressOnRails.configure do |c|
# ...
c.api_prefix = '/api'
end- Fork it ( https://github.com/shakacode/cypress-on-rails/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
The following companies support our open source projects, and ShakaCode uses their products!