Skip to content

Commit 2d6c2c8

Browse files
justin808claude
andcommitted
Add cypress-rails compatible rake tasks and server lifecycle hooks
This enhancement brings cypress-rails functionality to cypress-playwright-on-rails: - Added rake tasks for cypress:open, cypress:run, playwright:open, playwright:run - Implemented automatic Rails server management with dynamic port selection - Added server lifecycle hooks (before_server_start, after_server_start, etc.) - Added transactional test mode for automatic database rollback - Added state reset middleware for /cypress_rails_reset_state endpoint - Support for CYPRESS_RAILS_HOST and CYPRESS_RAILS_PORT environment variables These changes make cypress-playwright-on-rails a more complete replacement for cypress-rails, providing the same developer-friendly test execution experience while maintaining all the existing cypress-on-rails functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ac5d69f commit 2d6c2c8

File tree

8 files changed

+395
-0
lines changed

8 files changed

+395
-0
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55

66
---
77

8+
## [Unreleased]
9+
10+
### Added
11+
* **Rake tasks for test execution**: Added `cypress:open` and `cypress:run` rake tasks for seamless test execution, similar to cypress-rails functionality. Also added `playwright:open` and `playwright:run` tasks.
12+
* **Server lifecycle hooks**: Added configuration hooks for test server management:
13+
- `before_server_start`: Run code before Rails server starts
14+
- `after_server_start`: Run code after Rails server is ready
15+
- `after_transaction_start`: Run code after database transaction begins
16+
- `after_state_reset`: Run code after application state is reset
17+
- `before_server_stop`: Run code before Rails server stops
18+
* **State reset endpoint**: Added `/cypress_rails_reset_state` and `/__cypress__/reset_state` endpoints for compatibility with cypress-rails
19+
* **Transactional test mode**: Added support for automatic database transaction rollback between tests
20+
* **Environment configuration**: Support for `CYPRESS_RAILS_HOST` and `CYPRESS_RAILS_PORT` environment variables
21+
* **Automatic server management**: Test server automatically starts and stops with test execution
22+
23+
---
24+
825
## [1.18.0] — 2025-08-27
926
[Compare]: https://github.com/shakacode/cypress-playwright-on-rails/compare/v1.17.0...v1.18.0
1027

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,29 @@ Please use with extra caution if starting your local server on 0.0.0.0 or runnin
143143
144144
Getting started on your local environment
145145
146+
### Using Rake Tasks (Recommended)
147+
148+
The easiest way to run tests is using the provided rake tasks, which automatically manage the Rails server:
149+
150+
```shell
151+
# For Cypress
152+
bin/rails cypress:open # Opens Cypress test runner UI
153+
bin/rails cypress:run # Runs Cypress tests in headless mode
154+
155+
# For Playwright
156+
bin/rails playwright:open # Opens Playwright test runner UI
157+
bin/rails playwright:run # Runs Playwright tests in headless mode
158+
```
159+
160+
These tasks will:
161+
- Start the Rails test server automatically
162+
- Execute your tests
163+
- Stop the server when done
164+
165+
### Manual Server Management
166+
167+
You can also manage the server manually:
168+
146169
```shell
147170
# start rails
148171
CYPRESS=1 bin/rails server -p 5017
@@ -506,6 +529,44 @@ Consider VCR configuration in `cypress_helper.rb` to ignore hosts.
506529
All cassettes will be recorded and saved automatically, using the pattern `<vcs_cassettes_path>/graphql/<operation_name>`
507530

508531

532+
## Server Hooks Configuration
533+
534+
When using the rake tasks (`cypress:open`, `cypress:run`, `playwright:open`, `playwright:run`), you can configure lifecycle hooks to customize test server behavior:
535+
536+
```ruby
537+
CypressOnRails.configure do |c|
538+
# Run code before Rails server starts
539+
c.before_server_start = -> {
540+
puts "Preparing test environment..."
541+
}
542+
543+
# Run code after Rails server is ready
544+
c.after_server_start = -> {
545+
puts "Server is ready for testing!"
546+
}
547+
548+
# Run code after database transaction begins (transactional mode only)
549+
c.after_transaction_start = -> {
550+
# Load seed data that should be rolled back after tests
551+
}
552+
553+
# Run code after application state is reset
554+
c.after_state_reset = -> {
555+
Rails.cache.clear
556+
}
557+
558+
# Run code before Rails server stops
559+
c.before_server_stop = -> {
560+
puts "Cleaning up test environment..."
561+
}
562+
563+
# Configure server settings
564+
c.server_host = 'localhost' # or use ENV['CYPRESS_RAILS_HOST']
565+
c.server_port = 3001 # or use ENV['CYPRESS_RAILS_PORT']
566+
c.transactional_server = true # Enable automatic transaction rollback
567+
end
568+
```
569+
509570
## `before_request` configuration
510571

511572
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.

lib/cypress_on_rails/configuration.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ class Configuration
1010
attr_accessor :before_request
1111
attr_accessor :logger
1212
attr_accessor :vcr_options
13+
14+
# Server hooks for managing test lifecycle
15+
attr_accessor :before_server_start
16+
attr_accessor :after_server_start
17+
attr_accessor :after_transaction_start
18+
attr_accessor :after_state_reset
19+
attr_accessor :before_server_stop
20+
21+
# Server configuration
22+
attr_accessor :server_host
23+
attr_accessor :server_port
24+
attr_accessor :transactional_server
1325

1426
# Attributes for backwards compatibility
1527
def cypress_folder
@@ -38,6 +50,18 @@ def reset
3850
self.before_request = -> (request) {}
3951
self.logger = Logger.new(STDOUT)
4052
self.vcr_options = {}
53+
54+
# Server hooks
55+
self.before_server_start = nil
56+
self.after_server_start = nil
57+
self.after_transaction_start = nil
58+
self.after_state_reset = nil
59+
self.before_server_stop = nil
60+
61+
# Server configuration
62+
self.server_host = ENV.fetch('CYPRESS_RAILS_HOST', 'localhost')
63+
self.server_port = ENV.fetch('CYPRESS_RAILS_PORT', nil)
64+
self.transactional_server = true
4165
end
4266

4367
def tagged_logged

lib/cypress_on_rails/railtie.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33

44
module CypressOnRails
55
class Railtie < Rails::Railtie
6+
rake_tasks do
7+
load 'tasks/cypress.rake'
8+
end
69
initializer :setup_cypress_middleware, after: :load_config_initializers do |app|
710
if CypressOnRails.configuration.use_middleware?
811
require 'cypress_on_rails/middleware'
912
app.middleware.use Middleware
13+
14+
# Add state reset middleware for compatibility with cypress-rails
15+
require 'cypress_on_rails/state_reset_middleware'
16+
app.middleware.use StateResetMiddleware
1017
end
1118
if CypressOnRails.configuration.use_vcr_middleware?
1219
require 'cypress_on_rails/vcr/insert_eject_middleware'

lib/cypress_on_rails/server.rb

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
require 'socket'
2+
require 'timeout'
3+
require 'cypress_on_rails/configuration'
4+
5+
module CypressOnRails
6+
class Server
7+
attr_reader :host, :port, :framework, :install_folder
8+
9+
def initialize(options = {})
10+
config = CypressOnRails.configuration
11+
12+
@framework = options[:framework] || :cypress
13+
@host = options[:host] || config.server_host
14+
@port = options[:port] || config.server_port || find_available_port
15+
@port = @port.to_i if @port
16+
@install_folder = options[:install_folder] || config.install_folder || detect_install_folder
17+
@transactional = options.fetch(:transactional, config.transactional_server)
18+
end
19+
20+
def open
21+
start_server do
22+
run_command(open_command, "Opening #{framework} test runner")
23+
end
24+
end
25+
26+
def run
27+
start_server do
28+
result = run_command(run_command_str, "Running #{framework} tests")
29+
exit(result ? 0 : 1)
30+
end
31+
end
32+
33+
def init
34+
ensure_install_folder_exists
35+
puts "#{framework.to_s.capitalize} configuration initialized at #{install_folder}"
36+
end
37+
38+
private
39+
40+
def detect_install_folder
41+
# Check common locations for cypress/playwright installation
42+
possible_folders = ['e2e', 'spec/e2e', 'spec/cypress', 'spec/playwright', 'cypress', 'playwright']
43+
folder = possible_folders.find { |f| File.exist?(f) }
44+
folder || 'e2e'
45+
end
46+
47+
def ensure_install_folder_exists
48+
unless File.exist?(install_folder)
49+
puts "Creating #{install_folder} directory..."
50+
FileUtils.mkdir_p(install_folder)
51+
end
52+
end
53+
54+
def find_available_port
55+
server = TCPServer.new('127.0.0.1', 0)
56+
port = server.addr[1]
57+
server.close
58+
port
59+
end
60+
61+
def start_server(&block)
62+
config = CypressOnRails.configuration
63+
64+
run_hook(config.before_server_start)
65+
66+
ENV['CYPRESS'] = '1'
67+
ENV['RAILS_ENV'] = 'test'
68+
69+
server_pid = spawn_server
70+
71+
begin
72+
wait_for_server
73+
run_hook(config.after_server_start)
74+
75+
puts "Rails server started on #{base_url}"
76+
77+
if @transactional && defined?(ActiveRecord::Base)
78+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
79+
run_hook(config.after_transaction_start)
80+
end
81+
82+
yield
83+
84+
ensure
85+
run_hook(config.before_server_stop)
86+
87+
if @transactional && defined?(ActiveRecord::Base)
88+
ActiveRecord::Base.connection.rollback_transaction if ActiveRecord::Base.connection.transaction_open?
89+
end
90+
91+
stop_server(server_pid)
92+
ENV.delete('CYPRESS')
93+
end
94+
end
95+
96+
def spawn_server
97+
rails_command = if File.exist?('bin/rails')
98+
'bin/rails'
99+
else
100+
'bundle exec rails'
101+
end
102+
103+
server_command = "#{rails_command} server -p #{port} -b #{host}"
104+
105+
puts "Starting Rails server: #{server_command}"
106+
107+
spawn(server_command, out: $stdout, err: $stderr)
108+
end
109+
110+
def wait_for_server(timeout = 30)
111+
Timeout.timeout(timeout) do
112+
loop do
113+
begin
114+
TCPSocket.new(host, port).close
115+
break
116+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
117+
sleep 0.1
118+
end
119+
end
120+
end
121+
rescue Timeout::Error
122+
raise "Rails server failed to start on #{host}:#{port} after #{timeout} seconds"
123+
end
124+
125+
def stop_server(pid)
126+
if pid
127+
puts "Stopping Rails server (PID: #{pid})"
128+
Process.kill('TERM', pid)
129+
Process.wait(pid)
130+
end
131+
rescue Errno::ESRCH
132+
# Process already terminated
133+
end
134+
135+
def base_url
136+
"http://#{host}:#{port}"
137+
end
138+
139+
def open_command
140+
case framework
141+
when :cypress
142+
if command_exists?('yarn')
143+
"yarn cypress open --project #{install_folder} --config baseUrl=#{base_url}"
144+
elsif command_exists?('npx')
145+
"npx cypress open --project #{install_folder} --config baseUrl=#{base_url}"
146+
else
147+
"cypress open --project #{install_folder} --config baseUrl=#{base_url}"
148+
end
149+
when :playwright
150+
if command_exists?('yarn')
151+
"yarn playwright test --ui"
152+
elsif command_exists?('npx')
153+
"npx playwright test --ui"
154+
else
155+
"playwright test --ui"
156+
end
157+
end
158+
end
159+
160+
def run_command_str
161+
case framework
162+
when :cypress
163+
if command_exists?('yarn')
164+
"yarn cypress run --project #{install_folder} --config baseUrl=#{base_url}"
165+
elsif command_exists?('npx')
166+
"npx cypress run --project #{install_folder} --config baseUrl=#{base_url}"
167+
else
168+
"cypress run --project #{install_folder} --config baseUrl=#{base_url}"
169+
end
170+
when :playwright
171+
if command_exists?('yarn')
172+
"yarn playwright test"
173+
elsif command_exists?('npx')
174+
"npx playwright test"
175+
else
176+
"playwright test"
177+
end
178+
end
179+
end
180+
181+
def run_command(command, description)
182+
puts "#{description}: #{command}"
183+
system(command)
184+
end
185+
186+
def command_exists?(command)
187+
system("which #{command} > /dev/null 2>&1")
188+
end
189+
190+
def run_hook(hook)
191+
if hook && hook.respond_to?(:call)
192+
hook.call
193+
end
194+
end
195+
end
196+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module CypressOnRails
2+
class StateResetMiddleware
3+
def initialize(app)
4+
@app = app
5+
end
6+
7+
def call(env)
8+
if env['PATH_INFO'] == '/__cypress__/reset_state' || env['PATH_INFO'] == '/cypress_rails_reset_state'
9+
reset_application_state
10+
[200, { 'Content-Type' => 'text/plain' }, ['State reset completed']]
11+
else
12+
@app.call(env)
13+
end
14+
end
15+
16+
private
17+
18+
def reset_application_state
19+
config = CypressOnRails.configuration
20+
21+
# Run after_state_reset hook if configured
22+
run_hook(config.after_state_reset)
23+
24+
# Default state reset actions
25+
if defined?(DatabaseCleaner)
26+
DatabaseCleaner.clean_with(:truncation)
27+
elsif defined?(ActiveRecord::Base)
28+
ActiveRecord::Base.connection.tables.each do |table|
29+
next if table == 'schema_migrations' || table == 'ar_internal_metadata'
30+
ActiveRecord::Base.connection.execute("DELETE FROM #{table}")
31+
end
32+
end
33+
34+
# Clear Rails cache
35+
Rails.cache.clear if defined?(Rails) && Rails.cache
36+
37+
# Reset any class-level state
38+
ActiveSupport::Dependencies.clear if defined?(ActiveSupport::Dependencies)
39+
end
40+
41+
def run_hook(hook)
42+
hook.call if hook && hook.respond_to?(:call)
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)