cypress-on-rails 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +50 -0
- data/CHANGELOG.md +399 -98
- data/README.md +139 -19
- data/RELEASING.md +200 -0
- data/Rakefile +1 -4
- data/cypress-on-rails.gemspec +3 -2
- data/docs/BEST_PRACTICES.md +678 -0
- data/docs/DX_IMPROVEMENTS.md +163 -0
- data/docs/PLAYWRIGHT_GUIDE.md +554 -0
- data/docs/RELEASE.md +124 -0
- data/docs/TROUBLESHOOTING.md +351 -0
- data/docs/VCR_GUIDE.md +499 -0
- data/lib/cypress_on_rails/command_executor.rb +24 -0
- data/lib/cypress_on_rails/configuration.rb +32 -0
- data/lib/cypress_on_rails/railtie.rb +7 -0
- data/lib/cypress_on_rails/server.rb +258 -0
- data/lib/cypress_on_rails/state_reset_middleware.rb +58 -0
- data/lib/cypress_on_rails/version.rb +1 -1
- data/lib/generators/cypress_on_rails/install_generator.rb +2 -2
- data/lib/generators/cypress_on_rails/templates/config/initializers/cypress_on_rails.rb.erb +14 -2
- data/lib/generators/cypress_on_rails/templates/spec/cypress/e2e/rails_examples/using_factory_bot.cy.js +2 -2
- data/lib/generators/cypress_on_rails/templates/spec/cypress/e2e/rails_examples/using_scenarios.cy.js +1 -1
- data/lib/generators/cypress_on_rails/templates/spec/cypress/support/on-rails.js +1 -1
- data/lib/tasks/cypress.rake +33 -0
- data/rakelib/release.rake +80 -0
- data/rakelib/task_helpers.rb +23 -0
- data/rakelib/update_changelog.rake +63 -0
- data/spec/cypress_on_rails/configuration_spec.rb +6 -0
- data/spec/generators/install_generator_spec.rb +222 -0
- metadata +34 -4
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'cypress_on_rails/configuration'
|
|
6
|
+
|
|
7
|
+
module CypressOnRails
|
|
8
|
+
class Server
|
|
9
|
+
attr_reader :host, :port, :framework, :install_folder
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
config = CypressOnRails.configuration
|
|
13
|
+
|
|
14
|
+
@framework = options[:framework] || :cypress
|
|
15
|
+
@host = options[:host] || config.server_host
|
|
16
|
+
@port = options[:port] || config.server_port || find_available_port
|
|
17
|
+
@port = @port.to_i if @port
|
|
18
|
+
@install_folder = options[:install_folder] || config.install_folder || detect_install_folder
|
|
19
|
+
@transactional = options.fetch(:transactional, config.transactional_server)
|
|
20
|
+
# Process management: track PID and process group for proper cleanup
|
|
21
|
+
@server_pid = nil
|
|
22
|
+
@server_pgid = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def open
|
|
26
|
+
start_server do
|
|
27
|
+
run_command(open_command, "Opening #{framework} test runner")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
start_server do
|
|
33
|
+
result = run_command(run_command_args, "Running #{framework} tests")
|
|
34
|
+
exit(result ? 0 : 1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def init
|
|
39
|
+
ensure_install_folder_exists
|
|
40
|
+
puts "#{framework.to_s.capitalize} configuration initialized at #{install_folder}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def detect_install_folder
|
|
46
|
+
# Check common locations for cypress/playwright installation
|
|
47
|
+
possible_folders = ['e2e', 'spec/e2e', 'spec/cypress', 'spec/playwright', 'cypress', 'playwright']
|
|
48
|
+
folder = possible_folders.find { |f| File.exist?(File.expand_path(f)) }
|
|
49
|
+
folder || 'e2e'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ensure_install_folder_exists
|
|
53
|
+
unless File.exist?(install_folder)
|
|
54
|
+
puts "Creating #{install_folder} directory..."
|
|
55
|
+
FileUtils.mkdir_p(install_folder)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_available_port
|
|
60
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
61
|
+
port = server.addr[1]
|
|
62
|
+
server.close
|
|
63
|
+
port
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def start_server(&block)
|
|
67
|
+
config = CypressOnRails.configuration
|
|
68
|
+
|
|
69
|
+
run_hook(config.before_server_start)
|
|
70
|
+
|
|
71
|
+
ENV['CYPRESS'] = '1'
|
|
72
|
+
ENV['RAILS_ENV'] = 'test'
|
|
73
|
+
|
|
74
|
+
server_pid = spawn_server
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
wait_for_server
|
|
78
|
+
run_hook(config.after_server_start)
|
|
79
|
+
|
|
80
|
+
puts "Rails server started on #{base_url}"
|
|
81
|
+
|
|
82
|
+
if @transactional && defined?(ActiveRecord::Base)
|
|
83
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
|
84
|
+
run_hook(config.after_transaction_start)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
yield
|
|
88
|
+
|
|
89
|
+
ensure
|
|
90
|
+
run_hook(config.before_server_stop)
|
|
91
|
+
|
|
92
|
+
if @transactional && defined?(ActiveRecord::Base)
|
|
93
|
+
ActiveRecord::Base.connection.rollback_transaction if ActiveRecord::Base.connection.transaction_open?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
stop_server(server_pid)
|
|
97
|
+
ENV.delete('CYPRESS')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def spawn_server
|
|
102
|
+
rails_args = if File.exist?('bin/rails')
|
|
103
|
+
['bin/rails']
|
|
104
|
+
else
|
|
105
|
+
['bundle', 'exec', 'rails']
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
server_args = rails_args + ['server', '-p', port.to_s, '-b', host]
|
|
109
|
+
|
|
110
|
+
puts "Starting Rails server: #{server_args.join(' ')}"
|
|
111
|
+
|
|
112
|
+
@server_pid = spawn(*server_args, out: $stdout, err: $stderr, pgroup: true)
|
|
113
|
+
begin
|
|
114
|
+
@server_pgid = Process.getpgid(@server_pid)
|
|
115
|
+
rescue Errno::ESRCH => e
|
|
116
|
+
# Edge case: process terminated before we could get pgid
|
|
117
|
+
# This is OK - send_term_signal will fall back to single-process kill
|
|
118
|
+
CypressOnRails.configuration.logger.warn("Process #{@server_pid} terminated immediately after spawn: #{e.message}")
|
|
119
|
+
@server_pgid = nil
|
|
120
|
+
end
|
|
121
|
+
@server_pid
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def wait_for_server(timeout = 30)
|
|
125
|
+
Timeout.timeout(timeout) do
|
|
126
|
+
loop do
|
|
127
|
+
break if server_responding?
|
|
128
|
+
sleep 0.1
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
rescue Timeout::Error
|
|
132
|
+
raise "Rails server failed to start on #{host}:#{port} after #{timeout} seconds"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def server_responding?
|
|
136
|
+
config = CypressOnRails.configuration
|
|
137
|
+
readiness_path = config.server_readiness_path || '/'
|
|
138
|
+
timeout = config.server_readiness_timeout || 5
|
|
139
|
+
uri = URI("http://#{host}:#{port}#{readiness_path}")
|
|
140
|
+
|
|
141
|
+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: timeout, read_timeout: timeout) do |http|
|
|
142
|
+
http.get(uri.path)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Accept 200-399 (success and redirects), reject 404 and 5xx
|
|
146
|
+
# 3xx redirects are considered "ready" because the server is responding correctly
|
|
147
|
+
(200..399).cover?(response.code.to_i)
|
|
148
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, Errno::ETIMEDOUT, SocketError,
|
|
149
|
+
Net::OpenTimeout, Net::ReadTimeout, Net::HTTPBadResponse
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def stop_server(pid)
|
|
154
|
+
return unless pid
|
|
155
|
+
|
|
156
|
+
puts "Stopping Rails server (PID: #{pid})"
|
|
157
|
+
send_term_signal(pid)
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
Timeout.timeout(10) do
|
|
161
|
+
Process.wait(pid)
|
|
162
|
+
end
|
|
163
|
+
rescue Timeout::Error
|
|
164
|
+
CypressOnRails.configuration.logger.warn("Server did not terminate after TERM signal, sending KILL")
|
|
165
|
+
safe_kill_process('KILL', pid)
|
|
166
|
+
Process.wait(pid) rescue Errno::ESRCH
|
|
167
|
+
end
|
|
168
|
+
rescue Errno::ESRCH
|
|
169
|
+
# Process already terminated
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def send_term_signal(pid)
|
|
173
|
+
if @server_pgid && process_exists?(pid)
|
|
174
|
+
Process.kill('TERM', -@server_pgid)
|
|
175
|
+
else
|
|
176
|
+
safe_kill_process('TERM', pid)
|
|
177
|
+
end
|
|
178
|
+
rescue Errno::ESRCH, Errno::EPERM => e
|
|
179
|
+
CypressOnRails.configuration.logger.warn("Failed to kill process group #{@server_pgid}: #{e.message}, trying single process")
|
|
180
|
+
safe_kill_process('TERM', pid)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def process_exists?(pid)
|
|
184
|
+
return false unless pid
|
|
185
|
+
Process.kill(0, pid)
|
|
186
|
+
true
|
|
187
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
188
|
+
false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def safe_kill_process(signal, pid)
|
|
192
|
+
Process.kill(signal, pid) if pid
|
|
193
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
194
|
+
# Process already terminated or permission denied
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def base_url
|
|
198
|
+
"http://#{host}:#{port}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def open_command
|
|
202
|
+
case framework
|
|
203
|
+
when :cypress
|
|
204
|
+
if command_exists?('yarn')
|
|
205
|
+
['yarn', 'cypress', 'open', '--project', install_folder, '--config', "baseUrl=#{base_url}"]
|
|
206
|
+
elsif command_exists?('npx')
|
|
207
|
+
['npx', 'cypress', 'open', '--project', install_folder, '--config', "baseUrl=#{base_url}"]
|
|
208
|
+
else
|
|
209
|
+
['cypress', 'open', '--project', install_folder, '--config', "baseUrl=#{base_url}"]
|
|
210
|
+
end
|
|
211
|
+
when :playwright
|
|
212
|
+
if command_exists?('yarn')
|
|
213
|
+
['yarn', 'playwright', 'test', '--ui']
|
|
214
|
+
elsif command_exists?('npx')
|
|
215
|
+
['npx', 'playwright', 'test', '--ui']
|
|
216
|
+
else
|
|
217
|
+
['playwright', 'test', '--ui']
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def run_command_args
|
|
223
|
+
case framework
|
|
224
|
+
when :cypress
|
|
225
|
+
if command_exists?('yarn')
|
|
226
|
+
['yarn', 'cypress', 'run', '--project', install_folder, '--config', "baseUrl=#{base_url}"]
|
|
227
|
+
elsif command_exists?('npx')
|
|
228
|
+
['npx', 'cypress', 'run', '--project', install_folder, '--config', "baseUrl=#{base_url}"]
|
|
229
|
+
else
|
|
230
|
+
['cypress', 'run', '--project', install_folder, '--config', "baseUrl=#{base_url}"]
|
|
231
|
+
end
|
|
232
|
+
when :playwright
|
|
233
|
+
if command_exists?('yarn')
|
|
234
|
+
['yarn', 'playwright', 'test']
|
|
235
|
+
elsif command_exists?('npx')
|
|
236
|
+
['npx', 'playwright', 'test']
|
|
237
|
+
else
|
|
238
|
+
['playwright', 'test']
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def run_command(command_args, description)
|
|
244
|
+
puts "#{description}: #{command_args.join(' ')}"
|
|
245
|
+
system(*command_args)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def command_exists?(command)
|
|
249
|
+
system("which #{command} > /dev/null 2>&1")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def run_hook(hook)
|
|
253
|
+
if hook && hook.respond_to?(:call)
|
|
254
|
+
hook.call
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
# Default state reset actions
|
|
22
|
+
if defined?(DatabaseCleaner)
|
|
23
|
+
DatabaseCleaner.clean_with(:truncation)
|
|
24
|
+
elsif defined?(ActiveRecord::Base)
|
|
25
|
+
connection = ActiveRecord::Base.connection
|
|
26
|
+
|
|
27
|
+
# Use disable_referential_integrity if available for safer table clearing
|
|
28
|
+
if connection.respond_to?(:disable_referential_integrity)
|
|
29
|
+
connection.disable_referential_integrity do
|
|
30
|
+
connection.tables.each do |table|
|
|
31
|
+
next if table == 'schema_migrations' || table == 'ar_internal_metadata'
|
|
32
|
+
connection.execute("DELETE FROM #{connection.quote_table_name(table)}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
# Fallback to regular deletion with proper table name quoting
|
|
37
|
+
connection.tables.each do |table|
|
|
38
|
+
next if table == 'schema_migrations' || table == 'ar_internal_metadata'
|
|
39
|
+
connection.execute("DELETE FROM #{connection.quote_table_name(table)}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Clear Rails cache
|
|
45
|
+
Rails.cache.clear if defined?(Rails) && Rails.cache
|
|
46
|
+
|
|
47
|
+
# Reset any class-level state
|
|
48
|
+
ActiveSupport::Dependencies.clear if defined?(ActiveSupport::Dependencies)
|
|
49
|
+
|
|
50
|
+
# Run after_state_reset hook after cleanup is complete
|
|
51
|
+
run_hook(config.after_state_reset)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_hook(hook)
|
|
55
|
+
hook.call if hook && hook.respond_to?(:call)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -42,8 +42,8 @@ module CypressOnRails
|
|
|
42
42
|
|
|
43
43
|
def add_initial_files
|
|
44
44
|
template "config/initializers/cypress_on_rails.rb.erb", "config/initializers/cypress_on_rails.rb"
|
|
45
|
-
template "spec/e2e/e2e_helper.rb.erb", "#{options.install_folder}
|
|
46
|
-
directory 'spec/e2e/app_commands', "#{options.install_folder}
|
|
45
|
+
template "spec/e2e/e2e_helper.rb.erb", "#{options.install_folder}/e2e_helper.rb"
|
|
46
|
+
directory 'spec/e2e/app_commands', "#{options.install_folder}/app_commands"
|
|
47
47
|
if options.framework == 'cypress'
|
|
48
48
|
copy_file "spec/cypress/support/on-rails.js", "#{options.install_folder}/cypress/support/on-rails.js"
|
|
49
49
|
directory 'spec/cypress/e2e/rails_examples', "#{options.install_folder}/cypress/e2e/rails_examples"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
if defined?(CypressOnRails)
|
|
2
2
|
CypressOnRails.configure do |c|
|
|
3
3
|
c.api_prefix = "<%= options.api_prefix %>"
|
|
4
|
-
c.install_folder = File.expand_path("#{__dir__}/../../<%= options.install_folder
|
|
4
|
+
c.install_folder = File.expand_path("#{__dir__}/../../<%= options.install_folder %>")
|
|
5
5
|
# WARNING!! CypressOnRails can execute arbitrary ruby code
|
|
6
6
|
# please use with extra caution if enabling on hosted servers or starting your local server on 0.0.0.0
|
|
7
7
|
c.use_middleware = !Rails.env.production?
|
|
@@ -12,9 +12,21 @@ if defined?(CypressOnRails)
|
|
|
12
12
|
<% unless options.experimental %># <% end %> c.vcr_options = {
|
|
13
13
|
<% unless options.experimental %># <% end %> hook_into: :webmock,
|
|
14
14
|
<% unless options.experimental %># <% end %> default_cassette_options: { record: :once },
|
|
15
|
-
<% unless options.experimental %># <% end %> cassette_library_dir: File.expand_path("#{__dir__}/../../<%= options.install_folder
|
|
15
|
+
<% unless options.experimental %># <% end %> cassette_library_dir: File.expand_path("#{__dir__}/../../<%= options.install_folder %>/fixtures/vcr_cassettes")
|
|
16
16
|
<% unless options.experimental %># <% end %> }
|
|
17
17
|
c.logger = Rails.logger
|
|
18
|
+
|
|
19
|
+
# Server configuration for rake tasks (cypress:open, cypress:run, playwright:open, playwright:run)
|
|
20
|
+
# c.server_host = 'localhost' # or use ENV['CYPRESS_RAILS_HOST']
|
|
21
|
+
# c.server_port = 3001 # or use ENV['CYPRESS_RAILS_PORT']
|
|
22
|
+
# c.transactional_server = true # Enable automatic transaction rollback between tests
|
|
23
|
+
|
|
24
|
+
# Server lifecycle hooks for rake tasks
|
|
25
|
+
# c.before_server_start = -> { DatabaseCleaner.clean_with(:truncation) }
|
|
26
|
+
# c.after_server_start = -> { puts "Test server started on port #{CypressOnRails.configuration.server_port}" }
|
|
27
|
+
# c.after_transaction_start = -> { Rails.application.load_seed }
|
|
28
|
+
# c.after_state_reset = -> { Rails.cache.clear }
|
|
29
|
+
# c.before_server_stop = -> { puts "Stopping test server..." }
|
|
18
30
|
|
|
19
31
|
# If you want to enable a before_request logic, such as authentication, logging, sending metrics, etc.
|
|
20
32
|
# Refer to https://www.rubydoc.info/gems/rack/Rack/Request for the `request` argument.
|
|
@@ -9,7 +9,7 @@ describe('Rails using factory bot examples', function() {
|
|
|
9
9
|
])
|
|
10
10
|
cy.visit('/')
|
|
11
11
|
cy.get('table').find('tbody').should(($tbody) => {
|
|
12
|
-
// clean should
|
|
12
|
+
// clean should have removed these from other tests
|
|
13
13
|
expect($tbody).not.to.contain('Hello World')
|
|
14
14
|
|
|
15
15
|
expect($tbody).to.contain('Good bye Mars')
|
|
@@ -23,7 +23,7 @@ describe('Rails using factory bot examples', function() {
|
|
|
23
23
|
])
|
|
24
24
|
cy.visit('/')
|
|
25
25
|
cy.get('table').find('tbody').should(($tbody) => {
|
|
26
|
-
// clean should
|
|
26
|
+
// clean should have removed these from other tests
|
|
27
27
|
expect($tbody).to.contain('Hello World')
|
|
28
28
|
expect($tbody).not.to.contain('Good bye Mars')
|
|
29
29
|
})
|
data/lib/generators/cypress_on_rails/templates/spec/cypress/e2e/rails_examples/using_scenarios.cy.js
CHANGED
|
@@ -7,7 +7,7 @@ describe('Rails using scenarios examples', function() {
|
|
|
7
7
|
cy.appScenario('basic')
|
|
8
8
|
cy.visit('/')
|
|
9
9
|
cy.get('table').find('tbody').should(($tbody) => {
|
|
10
|
-
// clean should
|
|
10
|
+
// clean should have removed these from other tests
|
|
11
11
|
expect($tbody).not.to.contain('Good bye Mars')
|
|
12
12
|
expect($tbody).not.to.contain('Hello World')
|
|
13
13
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// CypressOnRails:
|
|
1
|
+
// CypressOnRails: don't remove these commands
|
|
2
2
|
Cypress.Commands.add('appCommands', function (body) {
|
|
3
3
|
Object.keys(body).forEach(key => body[key] === undefined ? delete body[key] : {});
|
|
4
4
|
const log = Cypress.log({ name: "APP", message: body, autoEnd: false })
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
namespace :cypress do
|
|
2
|
+
desc "Open Cypress test runner UI"
|
|
3
|
+
task :open => :environment do
|
|
4
|
+
require 'cypress_on_rails/server'
|
|
5
|
+
CypressOnRails::Server.new.open
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
desc "Run Cypress tests in headless mode"
|
|
9
|
+
task :run => :environment do
|
|
10
|
+
require 'cypress_on_rails/server'
|
|
11
|
+
CypressOnRails::Server.new.run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Initialize Cypress configuration"
|
|
15
|
+
task :init => :environment do
|
|
16
|
+
require 'cypress_on_rails/server'
|
|
17
|
+
CypressOnRails::Server.new.init
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
namespace :playwright do
|
|
22
|
+
desc "Open Playwright test runner UI"
|
|
23
|
+
task :open => :environment do
|
|
24
|
+
require 'cypress_on_rails/server'
|
|
25
|
+
CypressOnRails::Server.new(framework: :playwright).open
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Run Playwright tests in headless mode"
|
|
29
|
+
task :run => :environment do
|
|
30
|
+
require 'cypress_on_rails/server'
|
|
31
|
+
CypressOnRails::Server.new(framework: :playwright).run
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
require_relative "task_helpers"
|
|
5
|
+
|
|
6
|
+
class RaisingMessageHandler
|
|
7
|
+
def add_error(error)
|
|
8
|
+
raise error
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/BlockLength
|
|
13
|
+
|
|
14
|
+
desc("Releases the gem using the given version.
|
|
15
|
+
|
|
16
|
+
IMPORTANT: the gem version must be in valid rubygem format (no dashes).
|
|
17
|
+
|
|
18
|
+
This task depends on the gem-release (ruby gem) which is installed via `bundle install`
|
|
19
|
+
|
|
20
|
+
1st argument: The new version in rubygem format (no dashes). Pass no argument to
|
|
21
|
+
automatically perform a patch version bump.
|
|
22
|
+
2nd argument: Perform a dry run by passing 'true' as a second argument.
|
|
23
|
+
|
|
24
|
+
Note, accept defaults for rubygems options. Script will pause to get 2FA tokens.
|
|
25
|
+
|
|
26
|
+
Example: `rake release[2.1.0,false]`")
|
|
27
|
+
task :release, %i[gem_version dry_run] do |_t, args|
|
|
28
|
+
include CypressOnRails::TaskHelpers
|
|
29
|
+
|
|
30
|
+
# Check if there are uncommitted changes
|
|
31
|
+
unless `git status --porcelain`.strip.empty?
|
|
32
|
+
raise "You have uncommitted changes. Please commit or stash them before releasing."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
args_hash = args.to_hash
|
|
36
|
+
|
|
37
|
+
is_dry_run = args_hash[:dry_run] == 'true'
|
|
38
|
+
|
|
39
|
+
gem_version = args_hash.fetch(:gem_version, "")
|
|
40
|
+
|
|
41
|
+
# See https://github.com/svenfuchs/gem-release
|
|
42
|
+
sh_in_dir(gem_root, "git pull --rebase")
|
|
43
|
+
sh_in_dir(gem_root, "gem bump --no-commit --file lib/cypress_on_rails/version.rb #{gem_version.strip.empty? ? '' : %(--version #{gem_version})}")
|
|
44
|
+
|
|
45
|
+
# Read the actual version from the file after bump
|
|
46
|
+
load File.expand_path("../lib/cypress_on_rails/version.rb", __dir__)
|
|
47
|
+
actual_version = CypressOnRails::VERSION
|
|
48
|
+
|
|
49
|
+
# Update Gemfile.lock files
|
|
50
|
+
sh_in_dir(gem_root, "bundle install")
|
|
51
|
+
|
|
52
|
+
unless is_dry_run
|
|
53
|
+
# Commit the version bump
|
|
54
|
+
sh_in_dir(gem_root, "git add lib/cypress_on_rails/version.rb")
|
|
55
|
+
sh_in_dir(gem_root, "git commit -m \"Release v#{actual_version}\"")
|
|
56
|
+
|
|
57
|
+
# Tag the release
|
|
58
|
+
sh_in_dir(gem_root, "git tag v#{actual_version}")
|
|
59
|
+
|
|
60
|
+
# Push the commit and tag
|
|
61
|
+
sh_in_dir(gem_root, "git push && git push --tags")
|
|
62
|
+
|
|
63
|
+
# Release the new gem version
|
|
64
|
+
puts "Carefully add your OTP for Rubygems. If you get an error, run 'gem release' again."
|
|
65
|
+
sh_in_dir(gem_root, "gem release")
|
|
66
|
+
else
|
|
67
|
+
puts "DRY RUN: Would have committed, tagged v#{actual_version}, pushed, and released gem"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
msg = <<~MSG
|
|
71
|
+
Once you have successfully published, run these commands to update CHANGELOG.md:
|
|
72
|
+
|
|
73
|
+
bundle exec rake update_changelog
|
|
74
|
+
git commit -a -m 'Update CHANGELOG.md'
|
|
75
|
+
git push
|
|
76
|
+
MSG
|
|
77
|
+
puts msg
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# rubocop:enable Metrics/BlockLength
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CypressOnRails
|
|
4
|
+
module TaskHelpers
|
|
5
|
+
# Returns the root folder of the cypress-on-rails gem
|
|
6
|
+
def gem_root
|
|
7
|
+
File.expand_path("..", __dir__)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Executes a string or an array of strings in a shell in the given directory
|
|
11
|
+
def sh_in_dir(dir, *shell_commands)
|
|
12
|
+
Dir.chdir(dir) do
|
|
13
|
+
# Without `with_unbundled_env`, running bundle in the child directories won't correctly
|
|
14
|
+
# update the Gemfile.lock
|
|
15
|
+
Bundler.with_unbundled_env do
|
|
16
|
+
shell_commands.flatten.each do |shell_command|
|
|
17
|
+
sh(shell_command.strip)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
desc "Updates CHANGELOG.md inserting headers for the new version.
|
|
6
|
+
|
|
7
|
+
Argument: Git tag. Defaults to the latest tag."
|
|
8
|
+
|
|
9
|
+
task :update_changelog, %i[tag] do |_, args|
|
|
10
|
+
tag = args[:tag] || `git describe --tags --abbrev=0`.strip
|
|
11
|
+
|
|
12
|
+
# Remove 'v' prefix if present (e.g., v1.18.0 -> 1.18.0)
|
|
13
|
+
version = tag.start_with?('v') ? tag[1..-1] : tag
|
|
14
|
+
anchor = "[#{version}]"
|
|
15
|
+
|
|
16
|
+
changelog = File.read("CHANGELOG.md")
|
|
17
|
+
|
|
18
|
+
if changelog.include?(anchor)
|
|
19
|
+
puts "Tag #{version} is already documented in CHANGELOG.md, update manually if needed"
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
tag_date_output = `git show -s --format=%cs #{tag} 2>&1`
|
|
24
|
+
if $CHILD_STATUS.success?
|
|
25
|
+
tag_date = tag_date_output.split("\n").last.strip
|
|
26
|
+
else
|
|
27
|
+
abort("Failed to find tag #{tag}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# After "## [Unreleased]", insert new version header
|
|
31
|
+
unreleased_section = "## [Unreleased]"
|
|
32
|
+
new_version_header = "\n\n## #{anchor} - #{tag_date}"
|
|
33
|
+
|
|
34
|
+
if changelog.include?(unreleased_section)
|
|
35
|
+
changelog.sub!(unreleased_section, "#{unreleased_section}#{new_version_header}")
|
|
36
|
+
else
|
|
37
|
+
abort("Could not find '## [Unreleased]' section in CHANGELOG.md")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Find and update version comparison links at the bottom
|
|
41
|
+
# Pattern: [1.18.0]: https://github.com/shakacode/cypress-playwright-on-rails/compare/v1.17.0...v1.18.0
|
|
42
|
+
compare_link_prefix = "https://github.com/shakacode/cypress-playwright-on-rails/compare"
|
|
43
|
+
|
|
44
|
+
# Find the last version link to determine the previous version
|
|
45
|
+
last_version_match = changelog.match(/\[(\d+\.\d+\.\d+(?:\.\w+)?)\]:.*?compare\/v(\d+\.\d+\.\d+(?:\.\w+)?)\.\.\.v(\d+\.\d+\.\d+(?:\.\w+)?)/)
|
|
46
|
+
|
|
47
|
+
if last_version_match
|
|
48
|
+
last_version = last_version_match[1]
|
|
49
|
+
# Add new version link at the top of the version list
|
|
50
|
+
new_link = "#{anchor}: #{compare_link_prefix}/v#{last_version}...v#{version}"
|
|
51
|
+
# Insert after the "<!-- Version diff reference list -->" comment
|
|
52
|
+
changelog.sub!("<!-- Version diff reference list -->", "<!-- Version diff reference list -->\n#{new_link}")
|
|
53
|
+
else
|
|
54
|
+
puts "Warning: Could not find version comparison links. You may need to add the link manually."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
File.write("CHANGELOG.md", changelog)
|
|
58
|
+
puts "Updated CHANGELOG.md with an entry for #{version}"
|
|
59
|
+
puts "\nNext steps:"
|
|
60
|
+
puts "1. Edit CHANGELOG.md to add release notes under the [#{version}] section"
|
|
61
|
+
puts "2. Move content from [Unreleased] to [#{version}] if applicable"
|
|
62
|
+
puts "3. Review and commit the changes"
|
|
63
|
+
end
|
|
@@ -10,6 +10,8 @@ RSpec.describe CypressOnRails::Configuration do
|
|
|
10
10
|
expect(CypressOnRails.configuration.logger).to_not be_nil
|
|
11
11
|
expect(CypressOnRails.configuration.before_request).to_not be_nil
|
|
12
12
|
expect(CypressOnRails.configuration.vcr_options).to eq({})
|
|
13
|
+
expect(CypressOnRails.configuration.server_readiness_path).to eq('/')
|
|
14
|
+
expect(CypressOnRails.configuration.server_readiness_timeout).to eq(5)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
it 'can be configured' do
|
|
@@ -22,6 +24,8 @@ RSpec.describe CypressOnRails::Configuration do
|
|
|
22
24
|
config.logger = my_logger
|
|
23
25
|
config.before_request = before_request_lambda
|
|
24
26
|
config.vcr_options = { hook_into: :webmock }
|
|
27
|
+
config.server_readiness_path = '/health'
|
|
28
|
+
config.server_readiness_timeout = 10
|
|
25
29
|
end
|
|
26
30
|
expect(CypressOnRails.configuration.api_prefix).to eq('/api')
|
|
27
31
|
expect(CypressOnRails.configuration.install_folder).to eq('my/path')
|
|
@@ -29,5 +33,7 @@ RSpec.describe CypressOnRails::Configuration do
|
|
|
29
33
|
expect(CypressOnRails.configuration.logger).to eq(my_logger)
|
|
30
34
|
expect(CypressOnRails.configuration.before_request).to eq(before_request_lambda)
|
|
31
35
|
expect(CypressOnRails.configuration.vcr_options).to eq(hook_into: :webmock)
|
|
36
|
+
expect(CypressOnRails.configuration.server_readiness_path).to eq('/health')
|
|
37
|
+
expect(CypressOnRails.configuration.server_readiness_timeout).to eq(10)
|
|
32
38
|
end
|
|
33
39
|
end
|