ruby_raider 3.9.0 → 3.11.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/release.yml +18 -29
- data/.github/workflows/security.yml +68 -0
- data/.rubocop.yml +28 -0
- data/README.md +23 -4
- data/docs/superpowers/plans/2026-10-25-failure-video-replay.md +400 -0
- data/docs/superpowers/plans/2026-10-26-parity-cleanup.md +233 -0
- data/docs/superpowers/plans/2026-10-28-failure-card.md +295 -0
- data/docs/superpowers/specs/2026-10-25-failure-video-replay-design.md +214 -0
- data/docs/superpowers/specs/2026-10-28-failure-card-design.md +210 -0
- data/lib/commands/scaffolding_commands.rb +3 -3
- data/lib/commands/utility_commands.rb +36 -6
- data/lib/generators/cucumber/templates/cucumber.tt +1 -1
- data/lib/generators/cucumber/templates/feature.tt +5 -0
- data/lib/generators/cucumber/templates/partials/appium_env.tt +6 -0
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -0
- data/lib/generators/cucumber/templates/partials/watir_env.tt +22 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
- data/lib/generators/helper_generator.rb +12 -1
- data/lib/generators/infrastructure/azure_generator.rb +1 -0
- data/lib/generators/infrastructure/github_generator.rb +1 -0
- data/lib/generators/infrastructure/templates/azure_pipelines.tt +12 -1
- data/lib/generators/infrastructure/templates/github.tt +12 -0
- data/lib/generators/infrastructure/templates/github_appium.tt +23 -0
- data/lib/generators/invoke_generators.rb +20 -2
- data/lib/generators/rspec/rspec_generator.rb +0 -1
- data/lib/generators/rspec/templates/spec.tt +12 -0
- data/lib/generators/templates/common/gemfile.tt +1 -1
- data/lib/generators/templates/common/git_ignore.tt +7 -1
- data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
- data/lib/generators/templates/common/partials/web_config.tt +9 -1
- data/lib/generators/templates/common/read_me.tt +84 -2
- data/lib/generators/templates/helpers/browser_helper.tt +5 -1
- data/lib/generators/templates/helpers/driver_helper.tt +1 -0
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +4 -1
- data/lib/generators/templates/helpers/session_state.tt +206 -0
- data/lib/generators/templates/helpers/spec_helper.tt +28 -0
- data/lib/generators/templates/helpers/video_helper.tt +338 -171
- data/lib/ruby_raider/ffmpeg.rb +280 -0
- data/lib/ruby_raider.rb +2 -3
- data/lib/scaffolding/project_detector.rb +1 -3
- data/lib/scaffolding/scaffolding.rb +0 -1
- data/lib/utilities/utilities.rb +15 -2
- data/lib/version +1 -1
- data/ruby_raider.gemspec +4 -1
- data/sig/commands/utility_commands.rbs +4 -1
- data/sig/generators/invoke_generators.rbs +2 -3
- data/sig/ruby_raider/ffmpeg.rbs +23 -0
- data/sig/utilities/utilities.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +56 -1
- data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +28 -0
- data/spec/generators/video_helper_spec.rb +608 -0
- data/spec/integration/commands/browser_update_after_creation_spec.rb +7 -6
- data/spec/integration/commands/utility_commands_spec.rb +2 -2
- data/spec/integration/content/azure_content_spec.rb +33 -0
- data/spec/integration/content/ci_content_spec.rb +54 -0
- data/spec/integration/content/common_content_spec.rb +35 -0
- data/spec/integration/content/config_content_spec.rb +32 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +47 -1
- data/spec/integration/content/gemfile_content_spec.rb +2 -3
- data/spec/integration/content/helper_content_spec.rb +178 -0
- data/spec/integration/content/page_content_spec.rb +0 -1
- data/spec/integration/content/readme_content_spec.rb +79 -0
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/syntax_validation_spec.rb +15 -0
- data/spec/integration/content/test_content_spec.rb +27 -3
- data/spec/integration/end_to_end_features_spec.rb +0 -2
- data/spec/integration/end_to_end_spec.rb +275 -26
- data/spec/integration/generators/azure_generator_spec.rb +7 -6
- data/spec/integration/generators/common_generator_spec.rb +1 -1
- data/spec/integration/generators/helpers_generator_spec.rb +22 -0
- data/spec/integration/generators/invoke_generators_spec.rb +52 -0
- data/spec/integration/helpers/session_state_spec.rb +428 -0
- data/spec/integration/scaffolding_e2e_spec.rb +13 -24
- data/spec/scaffolding/scaffold_project_detector_spec.rb +0 -1
- data/spec/scaffolding/scaffolding_features_spec.rb +0 -1
- data/spec/system/selenium_spec.rb +0 -1
- data/spec/system/watir_spec.rb +0 -1
- data/spec/utilities/ffmpeg_spec.rb +424 -0
- data/spec/utilities/utilities_spec.rb +19 -2
- data/spec/utilities/video_command_spec.rb +120 -0
- metadata +65 -11
- data/lib/commands/adopt_commands.rb +0 -42
- data/lib/commands/loaded_commands.rb +0 -9
- data/lib/commands/plugin_commands.rb +0 -32
- data/sig/commands/adopt_commands.rbs +0 -8
- data/sig/commands/loaded_commands.rbs +0 -5
- data/sig/commands/plugin_commands.rbs +0 -9
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require_relative 'content_helper'
|
|
6
|
+
require_relative '../../../lib/ruby_raider/ffmpeg'
|
|
6
7
|
|
|
7
8
|
# UseRubyVersion@0 versionSpec '3.4' resolves against the hosted tool cache, which lists Ruby 3.4.x
|
|
8
9
|
# (3.2, 3.3, 3.4, 4.0): https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md
|
|
@@ -47,6 +48,33 @@ describe 'Azure Pipelines content' do
|
|
|
47
48
|
)
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
it 'caches the ffmpeg dir keyed on the ffmpeg version and Gemfile.lock, then installs ffmpeg, before the tests' do
|
|
52
|
+
steps = yml['jobs'][0]['steps']
|
|
53
|
+
cache = steps.index { |s| s['task'] == 'Cache@2' }
|
|
54
|
+
install = steps.index { |s| s['script'] == 'bundle exec raider u video install' }
|
|
55
|
+
tests = steps.index { |s| s['script'] == run_command }
|
|
56
|
+
expect([cache, install, tests]).to all(be_an(Integer))
|
|
57
|
+
expect(cache).to be < install
|
|
58
|
+
expect(install).to be < tests
|
|
59
|
+
expect(steps[cache]['inputs']).to eq(
|
|
60
|
+
'key' => "raider-ffmpeg-#{RubyRaider::Ffmpeg::MANIFEST[:version]} | \"$(Agent.OS)\" | Gemfile.lock",
|
|
61
|
+
'path' => '$(RAIDER_CACHE_DIR)'
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'caches RAIDER_CACHE_DIR in the pipeline workspace and keeps the ffmpeg install from failing the build' do
|
|
66
|
+
install = yml['jobs'][0]['steps'].find { |s| s['script'] == 'bundle exec raider u video install' }
|
|
67
|
+
expect(install['continueOnError']).to be(true)
|
|
68
|
+
expect(yml['variables']['RAIDER_CACHE_DIR']).to eq('$(Pipeline.Workspace)/.cache/raider')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'adds no cache or install step to the comment and gate jobs' do
|
|
72
|
+
yml['jobs'][1..].each do |job|
|
|
73
|
+
expect(job['steps'].map { |s| s['task'] || s['script'] })
|
|
74
|
+
.not_to include('Cache@2', 'bundle exec raider u video install')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
50
78
|
it 'passes exactly the Raider settings to the test step' do
|
|
51
79
|
step = yml['jobs'][0]['steps'].find { |s| s['script'] == run_command }
|
|
52
80
|
expect(step['env']).to eq('RAIDER_TOKEN' => '$(RAIDER_TOKEN)', 'RAIDER_ENDPOINT' => '$(RAIDER_ENDPOINT)',
|
|
@@ -96,12 +124,17 @@ describe 'Azure Pipelines content' do
|
|
|
96
124
|
expect(raw).not_to match(/allure/i)
|
|
97
125
|
end
|
|
98
126
|
|
|
127
|
+
it 'does not mention the saved login state' do
|
|
128
|
+
expect(raw).not_to include('.raider')
|
|
129
|
+
end
|
|
130
|
+
|
|
99
131
|
it 'documents Azure Pipelines in the README and no GitHub Actions setup' do
|
|
100
132
|
readme = read_generated(name, 'Readme.md')
|
|
101
133
|
expect(readme).to include('## Azure Pipelines', 'RAIDER_TOKEN', 'Contribute to pull requests',
|
|
102
134
|
'pool: { name: Default }', 'Build validation branch policy',
|
|
103
135
|
'same stage', 'raider_gate', 'RAIDER_GATE=1', 'RAIDER_SHARD_TOTAL', 'RAIDER_SHARD_INDEX')
|
|
104
136
|
expect(readme).not_to match(/GitHub Actions/i)
|
|
137
|
+
expect(readme).to include('`RAIDER_CACHE_DIR` to `$(Pipeline.Workspace)/.cache/raider`')
|
|
105
138
|
end
|
|
106
139
|
end
|
|
107
140
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
require_relative 'content_helper'
|
|
5
|
+
require_relative '../../../lib/ruby_raider/ffmpeg'
|
|
5
6
|
|
|
6
7
|
describe 'CI pipeline content' do
|
|
7
8
|
shared_examples 'valid github pipeline' do |project_name|
|
|
@@ -208,8 +209,45 @@ describe 'CI pipeline content' do
|
|
|
208
209
|
end
|
|
209
210
|
end
|
|
210
211
|
|
|
212
|
+
shared_examples 'github ffmpeg cache' do |project_name, file, test_jobs, runner = 'rspec'|
|
|
213
|
+
subject(:pipeline) { read_generated(project_name, ".github/workflows/#{file}") }
|
|
214
|
+
|
|
215
|
+
let(:jobs) { YAML.safe_load(pipeline)['jobs'] }
|
|
216
|
+
|
|
217
|
+
test_jobs.each do |job_name|
|
|
218
|
+
it "caches ~/.cache/raider keyed on the ffmpeg version and Gemfile.lock, then installs ffmpeg, before the tests in #{job_name}" do
|
|
219
|
+
steps = jobs[job_name]['steps']
|
|
220
|
+
cache = steps.index { |step| step['uses'] == 'actions/cache@v4' }
|
|
221
|
+
install = steps.index { |step| step['run'] == 'bundle exec raider u video install' }
|
|
222
|
+
tests = steps.index do |step|
|
|
223
|
+
step['run'].to_s.include?(runner) || step['with'].to_h['script'].to_s.include?(runner)
|
|
224
|
+
end
|
|
225
|
+
expect([cache, install, tests]).to all(be_an(Integer))
|
|
226
|
+
expect(cache).to be < install
|
|
227
|
+
expect(install).to be < tests
|
|
228
|
+
expect(steps[cache]['with']).to eq(
|
|
229
|
+
'path' => '~/.cache/raider',
|
|
230
|
+
'key' => "raider-ffmpeg-#{RubyRaider::Ffmpeg::MANIFEST[:version]}-${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}"
|
|
231
|
+
)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "keeps the ffmpeg install step from failing #{job_name}, since video is optional" do
|
|
235
|
+
install = jobs[job_name]['steps'].find { |step| step['run'] == 'bundle exec raider u video install' }
|
|
236
|
+
expect(install['continue-on-error']).to be(true)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'adds no cache or install step to the comment and gate jobs' do
|
|
241
|
+
jobs.slice('raider-comment', 'raider-gate').each_value do |job|
|
|
242
|
+
expect(job['steps'].map { |step| step['run'] || step['uses'] })
|
|
243
|
+
.not_to include('actions/cache@v4', 'bundle exec raider u video install')
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
211
248
|
context 'with rspec and selenium' do
|
|
212
249
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
250
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build]
|
|
213
251
|
include_examples 'valid github pipeline', name
|
|
214
252
|
include_examples 'github rspec pipeline', name
|
|
215
253
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
@@ -221,6 +259,7 @@ describe 'CI pipeline content' do
|
|
|
221
259
|
|
|
222
260
|
context 'with cucumber and selenium' do
|
|
223
261
|
name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
262
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build], 'cucumber'
|
|
224
263
|
include_examples 'valid github pipeline', name
|
|
225
264
|
include_examples 'github cucumber pipeline', name
|
|
226
265
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
@@ -232,6 +271,7 @@ describe 'CI pipeline content' do
|
|
|
232
271
|
|
|
233
272
|
context 'with rspec and watir' do
|
|
234
273
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
274
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build]
|
|
235
275
|
include_examples 'valid github pipeline', name
|
|
236
276
|
include_examples 'github rspec pipeline', name
|
|
237
277
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
@@ -243,6 +283,7 @@ describe 'CI pipeline content' do
|
|
|
243
283
|
|
|
244
284
|
context 'with cucumber and watir' do
|
|
245
285
|
name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
286
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build], 'cucumber'
|
|
246
287
|
include_examples 'valid github pipeline', name
|
|
247
288
|
include_examples 'github cucumber pipeline', name
|
|
248
289
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
@@ -261,6 +302,7 @@ describe 'CI pipeline content' do
|
|
|
261
302
|
context "with rspec and #{automation}" do
|
|
262
303
|
name = "#{FrameworkIndex::RSPEC}_#{automation}"
|
|
263
304
|
include_examples 'github appium shards', name, 'rspec', appium_shards[automation]
|
|
305
|
+
include_examples 'github ffmpeg cache', name, 'appium_pipeline.yml', test_jobs
|
|
264
306
|
include_examples 'github raider-comment job', name, 'appium_pipeline.yml', test_jobs
|
|
265
307
|
include_examples 'github raider-gate job', name, 'appium_pipeline.yml', test_jobs
|
|
266
308
|
include_examples 'github triggers', name, 'appium_pipeline.yml'
|
|
@@ -270,10 +312,22 @@ describe 'CI pipeline content' do
|
|
|
270
312
|
context "with cucumber and #{automation}" do
|
|
271
313
|
name = "#{FrameworkIndex::CUCUMBER}_#{automation}"
|
|
272
314
|
include_examples 'github appium shards', name, 'cucumber', appium_shards[automation]
|
|
315
|
+
include_examples 'github ffmpeg cache', name, 'appium_pipeline.yml', test_jobs, 'cucumber'
|
|
273
316
|
include_examples 'github triggers', name, 'appium_pipeline.yml'
|
|
274
317
|
include_examples 'github raider wiring', name, 'appium_pipeline.yml', 'cucumber'
|
|
275
318
|
include_examples 'github raider-comment job', name, 'appium_pipeline.yml', test_jobs
|
|
276
319
|
include_examples 'github raider-gate job', name, 'appium_pipeline.yml', test_jobs
|
|
277
320
|
end
|
|
278
321
|
end
|
|
322
|
+
|
|
323
|
+
context 'with the saved login state' do
|
|
324
|
+
workflows = FRAMEWORKS.product(%w[selenium watir]).map { |f, a| ["#{f}_#{a}", 'test_pipeline.yml'] } +
|
|
325
|
+
FRAMEWORKS.product(%w[android ios cross_platform]).map { |f, a| ["#{f}_#{a}", 'appium_pipeline.yml'] }
|
|
326
|
+
|
|
327
|
+
workflows.each do |name, file|
|
|
328
|
+
it "keeps .raider/auth out of #{name}/#{file}" do
|
|
329
|
+
expect(read_generated(name, ".github/workflows/#{file}")).not_to include('.raider')
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
279
333
|
end
|
|
@@ -133,6 +133,25 @@ describe 'Common file content' do
|
|
|
133
133
|
it 'includes raider-results' do
|
|
134
134
|
expect(gitignore).to include('raider-results')
|
|
135
135
|
end
|
|
136
|
+
|
|
137
|
+
it 'includes rerun.txt' do
|
|
138
|
+
expect(gitignore).to include('rerun.txt')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
shared_examples 'web gitignore for saved logins' do |project_name|
|
|
143
|
+
subject(:lines) { read_generated(project_name, '.gitignore').lines.map(&:strip) }
|
|
144
|
+
|
|
145
|
+
it 'ignores .raider/auth/ but not the whole .raider/' do
|
|
146
|
+
expect(lines).to include('.raider/auth/')
|
|
147
|
+
expect(lines).not_to include('.raider/', '.raider')
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
shared_examples 'mobile gitignore without saved logins' do |project_name|
|
|
152
|
+
it 'has no .raider line' do
|
|
153
|
+
expect(read_generated(project_name, '.gitignore')).not_to include('.raider')
|
|
154
|
+
end
|
|
136
155
|
end
|
|
137
156
|
|
|
138
157
|
# --- Spec helper retry config (C4) ---
|
|
@@ -187,6 +206,10 @@ describe 'Common file content' do
|
|
|
187
206
|
it 'includes --retry flag' do
|
|
188
207
|
expect(cucumber_yml).to include('--retry')
|
|
189
208
|
end
|
|
209
|
+
|
|
210
|
+
it 'writes failed scenarios to rerun.txt for Re-run Failed' do
|
|
211
|
+
expect(cucumber_yml).to include('--format rerun --out rerun.txt')
|
|
212
|
+
end
|
|
190
213
|
end
|
|
191
214
|
|
|
192
215
|
# --- RSpec contexts ---
|
|
@@ -199,6 +222,7 @@ describe 'Common file content' do
|
|
|
199
222
|
it_behaves_like 'rspec gitignore', name
|
|
200
223
|
it_behaves_like 'spec helper with retry config', name
|
|
201
224
|
it_behaves_like 'gemfile with rspec-retry', name
|
|
225
|
+
it_behaves_like 'web gitignore for saved logins', name
|
|
202
226
|
end
|
|
203
227
|
|
|
204
228
|
context 'with rspec and watir' do
|
|
@@ -209,6 +233,7 @@ describe 'Common file content' do
|
|
|
209
233
|
it_behaves_like 'rspec gitignore', name
|
|
210
234
|
it_behaves_like 'spec helper with retry config', name
|
|
211
235
|
it_behaves_like 'gemfile with rspec-retry', name
|
|
236
|
+
it_behaves_like 'web gitignore for saved logins', name
|
|
212
237
|
end
|
|
213
238
|
|
|
214
239
|
# --- Cucumber contexts ---
|
|
@@ -221,6 +246,7 @@ describe 'Common file content' do
|
|
|
221
246
|
it_behaves_like 'non-rspec gitignore', name
|
|
222
247
|
it_behaves_like 'gemfile without rspec-retry', name
|
|
223
248
|
it_behaves_like 'cucumber config with retry', name
|
|
249
|
+
it_behaves_like 'web gitignore for saved logins', name
|
|
224
250
|
end
|
|
225
251
|
|
|
226
252
|
context 'with cucumber and watir' do
|
|
@@ -231,5 +257,14 @@ describe 'Common file content' do
|
|
|
231
257
|
it_behaves_like 'non-rspec gitignore', name
|
|
232
258
|
it_behaves_like 'gemfile without rspec-retry', name
|
|
233
259
|
it_behaves_like 'cucumber config with retry', name
|
|
260
|
+
it_behaves_like 'web gitignore for saved logins', name
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
%w[android ios cross_platform].each do |automation|
|
|
264
|
+
%w[rspec cucumber].each do |framework|
|
|
265
|
+
context "with #{framework} and #{automation}" do
|
|
266
|
+
it_behaves_like 'mobile gitignore without saved logins', "#{framework}_#{automation}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
234
269
|
end
|
|
235
270
|
end
|
|
@@ -51,28 +51,52 @@ describe 'Config file content' do
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
shared_examples 'video config' do |project_name|
|
|
55
|
+
subject(:config) { YAML.safe_load(read_generated(project_name, 'config/config.yml'), permitted_classes: [Symbol]) }
|
|
56
|
+
|
|
57
|
+
it 'has video enabled at 5 fps and 1280 px max width' do
|
|
58
|
+
expect(config['video']).to eq('enabled' => true, 'fps' => 5, 'max_width' => 1280)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
shared_examples 'login state config' do |project_name|
|
|
63
|
+
it 'has a login_state max_age of an hour' do
|
|
64
|
+
config = YAML.safe_load(read_generated(project_name, 'config/config.yml'), permitted_classes: [Symbol])
|
|
65
|
+
expect(config['login_state']).to eq('max_age' => 3600)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
# --- Web config per automation ---
|
|
55
70
|
|
|
56
71
|
context 'with rspec and selenium' do
|
|
57
72
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
58
73
|
include_examples 'valid web config', name
|
|
59
74
|
include_examples 'selenium config with timeout', name
|
|
75
|
+
include_examples 'video config', name
|
|
76
|
+
include_examples 'login state config', name
|
|
60
77
|
end
|
|
61
78
|
|
|
62
79
|
context 'with rspec and watir' do
|
|
63
80
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
64
81
|
include_examples 'valid web config', name
|
|
65
82
|
include_examples 'config without driver_options', name
|
|
83
|
+
include_examples 'login state config', name
|
|
66
84
|
end
|
|
67
85
|
|
|
68
86
|
context 'with cucumber and selenium' do
|
|
69
87
|
name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
70
88
|
include_examples 'valid web config', name
|
|
71
89
|
include_examples 'selenium config with timeout', name
|
|
90
|
+
include_examples 'login state config', name
|
|
72
91
|
end
|
|
73
92
|
|
|
74
93
|
# --- Mobile config ---
|
|
75
94
|
|
|
95
|
+
# Single-platform Appium projects have no config.yml; VideoHelper falls back to its defaults.
|
|
96
|
+
context 'with rspec and cross platform' do
|
|
97
|
+
include_examples 'video config', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
98
|
+
end
|
|
99
|
+
|
|
76
100
|
context 'with rspec and appium android' do
|
|
77
101
|
subject(:caps) { read_generated("#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}", 'config/capabilities.yml') }
|
|
78
102
|
|
|
@@ -86,4 +110,12 @@ describe 'Config file content' do
|
|
|
86
110
|
expect(caps).to match(/platformName/i)
|
|
87
111
|
end
|
|
88
112
|
end
|
|
113
|
+
|
|
114
|
+
%w[android ios cross_platform].each do |automation|
|
|
115
|
+
it "has no login_state in the #{automation} config" do
|
|
116
|
+
Dir.glob(File.join("#{FrameworkIndex::RSPEC}_#{automation}", 'config', '*.yml')).each do |file|
|
|
117
|
+
expect(File.read(file)).not_to include('login_state')
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
89
121
|
end
|
|
@@ -36,7 +36,7 @@ describe 'Cucumber raider_reporter wiring' do
|
|
|
36
36
|
|
|
37
37
|
it 'quits the driver in an ensure of the After block, without a redundant begin' do
|
|
38
38
|
quit = %w[android ios cross_platform].include?(automation) ? 'driver.quit_driver' : quit_call
|
|
39
|
-
expect(hook).to match(/^\s*After do \|scenario\|\n.*\n^ensure\n\s+#{Regexp.escape(quit)}\nend/m)
|
|
39
|
+
expect(hook).to match(/^\s*After do \|scenario\|\n.*\n^ensure\n.*\n\s+#{Regexp.escape(quit)}\nend/m)
|
|
40
40
|
expect(hook).not_to match(/^\s+begin$/)
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -47,6 +47,27 @@ describe 'Cucumber raider_reporter wiring' do
|
|
|
47
47
|
expect(env.index(attach)).to be < env.index(/driver\.quit|browser\.quit/, env.index(attach))
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
context 'with video recording' do
|
|
51
|
+
target = automation == 'watir' ? 'browser' : 'driver'
|
|
52
|
+
video_type = %w[selenium watir].include?(automation) ? ':webm' : ':mp4'
|
|
53
|
+
|
|
54
|
+
it 'requires video_helper' do
|
|
55
|
+
expect(env).to include("require_relative '../../helpers/video_helper'")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'starts the recorder in Before' do
|
|
59
|
+
expect(env[/^Before do.*?^end$/m]).to include("@video_recorder = VideoHelper.recorder_for(#{target})", '@video_recorder.start')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'stops with keep: scenario.failed? before quitting, attaches the path and cleans up in the ensure' do
|
|
63
|
+
expect(hook).to include('video_path = @video_recorder&.stop(keep: scenario.failed?)')
|
|
64
|
+
expect(hook).to include("RaiderReporter.attach('video', video_path, type: #{video_type}) if video_path")
|
|
65
|
+
expect(hook.index('@video_recorder&.stop')).to be < hook.index("RaiderReporter.attach('video'")
|
|
66
|
+
expect(hook.index("RaiderReporter.attach('video'")).to be < hook.index(/^ensure/)
|
|
67
|
+
expect(hook.index(/^ensure/)).to be < hook.index('@video_recorder&.cleanup')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
50
71
|
it 'does not generate the allure helper' do
|
|
51
72
|
expect(File).not_to exist(File.join(project_name, 'helpers/allure_helper.rb'))
|
|
52
73
|
end
|
|
@@ -62,3 +83,28 @@ describe 'Cucumber raider_reporter wiring' do
|
|
|
62
83
|
end
|
|
63
84
|
end
|
|
64
85
|
end
|
|
86
|
+
|
|
87
|
+
describe 'Cucumber login reuse wiring' do
|
|
88
|
+
{ 'selenium' => 'driver', 'watir' => 'browser.driver' }.each do |automation, target|
|
|
89
|
+
context "with cucumber and #{automation}" do
|
|
90
|
+
let(:env) { read_generated("#{FrameworkIndex::CUCUMBER}_#{automation}", 'features/support/env.rb') }
|
|
91
|
+
|
|
92
|
+
it 'requires session_state' do
|
|
93
|
+
expect(env).to include("require_relative '../../helpers/session_state'")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "reuses the saved login in Before('@logged_in'), defined after the plain Before" do
|
|
97
|
+
expect(env).to include("Before('@logged_in') do", "SessionState.new(#{target}).reuse(",
|
|
98
|
+
"UserFactory.for('registered user')")
|
|
99
|
+
expect(env.index("Before('@logged_in')")).to be > env.index(/^Before do/)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
%w[android ios cross_platform].each do |automation|
|
|
105
|
+
it "does not mention session_state with cucumber and #{automation}" do
|
|
106
|
+
env = read_generated("#{FrameworkIndex::CUCUMBER}_#{automation}", 'features/support/env.rb')
|
|
107
|
+
expect(env).not_to include('session_state')
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -127,7 +127,7 @@ describe 'Gemfile content' do
|
|
|
127
127
|
let(:gemfile) { read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'Gemfile') }
|
|
128
128
|
|
|
129
129
|
it 'includes raider_reporter' do
|
|
130
|
-
expect(gemfile).to include("gem 'raider_reporter', '~> 0.
|
|
130
|
+
expect(gemfile).to include("gem 'raider_reporter', '~> 0.10'")
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
it 'has no allure gems' do
|
|
@@ -143,7 +143,7 @@ describe 'Gemfile content' do
|
|
|
143
143
|
let(:gemfile) { read_generated("#{FrameworkIndex::CUCUMBER}_#{automation}", 'Gemfile') }
|
|
144
144
|
|
|
145
145
|
it 'includes raider_reporter' do
|
|
146
|
-
expect(gemfile).to include("gem 'raider_reporter', '~> 0.
|
|
146
|
+
expect(gemfile).to include("gem 'raider_reporter', '~> 0.10'")
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
it 'drops allure-cucumber' do
|
|
@@ -156,5 +156,4 @@ describe 'Gemfile content' do
|
|
|
156
156
|
end
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
|
-
|
|
160
159
|
end
|
|
@@ -139,6 +139,43 @@ describe 'Helper file content' do
|
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
shared_examples 'video spec helper wiring' do |project_name, recorder_target, video_type|
|
|
143
|
+
subject(:helper) { read_generated(project_name, 'helpers/spec_helper.rb') }
|
|
144
|
+
|
|
145
|
+
let(:before_hook) { helper[/config\.before\(:each\) do.*?^ end$/m] }
|
|
146
|
+
let(:after_hook) { helper[/config\.after\(:each\).*\z/m] }
|
|
147
|
+
|
|
148
|
+
it 'requires video_helper' do
|
|
149
|
+
expect(helper).to include("require_relative 'video_helper'")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'starts a recorder in before(:each), after the viewport resize or driver start' do
|
|
153
|
+
start = "@video_recorder = VideoHelper.recorder_for(#{recorder_target})"
|
|
154
|
+
expect(before_hook).to include(start, '@video_recorder.start')
|
|
155
|
+
setup = before_hook[/driver\.start_driver|\.window\.maximize/]
|
|
156
|
+
expect(before_hook.index(setup)).to be < before_hook.index(start)
|
|
157
|
+
expect(before_hook.index(start)).to be < before_hook.index('@video_recorder.start')
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'attaches "video" only when example.exception && VideoHelper.final_attempt?(example)' do
|
|
161
|
+
expect(after_hook).to match(/example\.exception\s*(\?|&&)\s*VideoHelper\.final_attempt\?\(example\)/)
|
|
162
|
+
expect(after_hook).to include("RaiderReporter.attach('video', video_path, type: #{video_type}) if video_path")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'stops and attaches, then cleans up and quits the driver in the ensure, so the browser closes if an attach raises' do
|
|
166
|
+
order = [after_hook.index('@video_recorder&.stop(keep: keep_video)'), after_hook.index("RaiderReporter.attach('video'"),
|
|
167
|
+
after_hook.index("\n ensure\n"), after_hook.index('@video_recorder&.cleanup'),
|
|
168
|
+
after_hook.index(/driver\.quit|browser\.quit/)]
|
|
169
|
+
expect(order).to all(be_an(Integer)).and eq(order.compact.sort)
|
|
170
|
+
expect(after_hook.scan(/driver\.quit|browser\.quit/).size).to eq(1)
|
|
171
|
+
expect(after_hook[order[2]..]).to match(/\n @video_recorder&\.cleanup\n (driver|browser)\.quit(_driver)?\n end\n/)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'keeps the screenshot attachment' do
|
|
175
|
+
expect(after_hook).to include("RaiderReporter.attach('screenshot', screenshot_path, type: :png)")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
142
179
|
shared_examples 'selenium spec helper' do |project_name|
|
|
143
180
|
subject(:helper) { read_generated(project_name, 'helpers/spec_helper.rb') }
|
|
144
181
|
|
|
@@ -171,6 +208,113 @@ describe 'Helper file content' do
|
|
|
171
208
|
end
|
|
172
209
|
end
|
|
173
210
|
|
|
211
|
+
# --- Video helper ---
|
|
212
|
+
|
|
213
|
+
shared_examples 'valid video helper' do |project_name|
|
|
214
|
+
subject(:helper) { read_generated(project_name, 'helpers/video_helper.rb') }
|
|
215
|
+
|
|
216
|
+
it 'has frozen_string_literal' do
|
|
217
|
+
expect(helper).to have_frozen_string_literal
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'has valid Ruby syntax' do
|
|
221
|
+
expect(helper).to have_valid_ruby_syntax
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'defines VideoHelper with enabled?, recorder_for and disable!' do
|
|
225
|
+
expect(helper).to match(/module VideoHelper/)
|
|
226
|
+
expect(helper).to match(/def enabled\?/)
|
|
227
|
+
expect(helper).to match(/def recorder_for\(/)
|
|
228
|
+
expect(helper).to match(/def disable!\(/)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'has no ScreenCaptureRecorder and no CdpRecorder' do
|
|
232
|
+
expect(helper).not_to include('ScreenCaptureRecorder')
|
|
233
|
+
expect(helper).not_to include('CdpRecorder')
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
shared_examples 'web video helper' do |project_name|
|
|
238
|
+
subject(:helper) { read_generated(project_name, 'helpers/video_helper.rb') }
|
|
239
|
+
|
|
240
|
+
it 'defines a FrameRecorder and a Selenium FrameListener' do
|
|
241
|
+
expect(helper).to include('class FrameRecorder')
|
|
242
|
+
expect(helper).to include('class FrameListener < Selenium::WebDriver::Support::AbstractEventListener')
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'captures only after navigate, click and change_value_of' do
|
|
246
|
+
hooks = helper.scan(/def ((?:after|before)_\w+)/).flatten
|
|
247
|
+
expect(hooks).to contain_exactly('after_navigate_to', 'after_click', 'after_change_value_of')
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'encodes WebM with libvpx' do
|
|
251
|
+
expect(helper).to include("'libvpx'", 'video.webm')
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'loads ffmpeg lazily from the ruby_raider gem' do
|
|
255
|
+
expect(helper).to include("require 'ruby_raider/ffmpeg'")
|
|
256
|
+
expect(helper).not_to match(/^require 'ruby_raider/)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'has no AppiumRecorder' do
|
|
260
|
+
expect(helper).not_to include('AppiumRecorder')
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
shared_examples 'mobile video helper' do |project_name|
|
|
265
|
+
subject(:helper) { read_generated(project_name, 'helpers/video_helper.rb') }
|
|
266
|
+
|
|
267
|
+
it 'defines an AppiumRecorder writing mp4' do
|
|
268
|
+
expect(helper).to include('class AppiumRecorder', 'start_recording_screen', 'video.mp4')
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'has no FrameRecorder or listener' do
|
|
272
|
+
expect(helper).not_to include('FrameRecorder')
|
|
273
|
+
expect(helper).not_to include('AbstractEventListener')
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it 'does not provision ffmpeg on this machine (Appium records on its own host)' do
|
|
277
|
+
expect(helper).not_to include('ruby_raider/ffmpeg', 'Ffmpeg.ensure!', 'ffmpeg_path')
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
shared_examples 'video listener wiring' do |project_name, helper_file|
|
|
282
|
+
subject(:helper) { read_generated(project_name, "helpers/#{helper_file}") }
|
|
283
|
+
|
|
284
|
+
it 'requires video_helper' do
|
|
285
|
+
expect(helper).to include("require_relative 'video_helper'")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'passes VideoHelper.listener only when it is not nil' do
|
|
289
|
+
expect(helper).to include('VideoHelper.listener')
|
|
290
|
+
expect(helper).to match(/\[:listener\] = listener if listener/)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
context 'with rspec and selenium video' do
|
|
295
|
+
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
296
|
+
it_behaves_like 'valid video helper', name
|
|
297
|
+
it_behaves_like 'web video helper', name
|
|
298
|
+
it_behaves_like 'video listener wiring', name, 'driver_helper.rb'
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
context 'with cucumber and watir video' do
|
|
302
|
+
name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
303
|
+
it_behaves_like 'valid video helper', name
|
|
304
|
+
it_behaves_like 'web video helper', name
|
|
305
|
+
it_behaves_like 'video listener wiring', name, 'browser_helper.rb'
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
context 'with rspec and ios video' do
|
|
309
|
+
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
|
|
310
|
+
it_behaves_like 'valid video helper', name
|
|
311
|
+
it_behaves_like 'mobile video helper', name
|
|
312
|
+
|
|
313
|
+
it 'does not wire a listener into the appium driver helper' do
|
|
314
|
+
expect(read_generated(name, 'helpers/driver_helper.rb')).not_to include('listener')
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
174
318
|
# --- RSpec + automation contexts ---
|
|
175
319
|
|
|
176
320
|
context 'with rspec and selenium' do
|
|
@@ -211,6 +355,8 @@ describe 'Helper file content' do
|
|
|
211
355
|
'watir' => 'browser.screenshot.save(screenshot_path)'
|
|
212
356
|
}.fetch(automation, 'driver.screenshot(screenshot_path)')
|
|
213
357
|
it_behaves_like 'raider reporter spec helper', name, screenshot_call
|
|
358
|
+
it_behaves_like 'video spec helper wiring', name, automation == 'watir' ? 'browser' : 'driver',
|
|
359
|
+
%w[selenium watir].include?(automation) ? ':webm' : ':mp4'
|
|
214
360
|
end
|
|
215
361
|
|
|
216
362
|
context "with cucumber and #{automation}" do
|
|
@@ -219,4 +365,36 @@ describe 'Helper file content' do
|
|
|
219
365
|
end
|
|
220
366
|
end
|
|
221
367
|
end
|
|
368
|
+
|
|
369
|
+
# --- Opt-in login reuse ---
|
|
370
|
+
|
|
371
|
+
{ 'selenium' => 'driver', 'watir' => 'browser.driver' }.each do |automation, target|
|
|
372
|
+
context "with rspec and #{automation} login reuse" do
|
|
373
|
+
subject(:helper) { read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'helpers/spec_helper.rb') }
|
|
374
|
+
|
|
375
|
+
it 'requires session_state and opts in through the :logged_in tag' do
|
|
376
|
+
expect(helper).to include("require_relative 'session_state'", 'config.before(:each, :logged_in)')
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it 'reuses the saved login for the driver, falling back to the real login' do
|
|
380
|
+
expect(helper).to include("SessionState.new(#{target}).reuse(")
|
|
381
|
+
expect(helper).to include("user = ModelFactory.for('users')['registered user']", ".login(user['username'], user['password'])")
|
|
382
|
+
expect(helper).to include("header.customer_name == \"Welcome back \#{user['name']}\"")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
it 'requires the models and pages the login needs' do
|
|
386
|
+
expect(helper).to include("require_relative '../models/model_factory'",
|
|
387
|
+
"require_relative '../page_objects/pages/login'",
|
|
388
|
+
"require_relative '../page_objects/pages/account'")
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
%w[android ios cross_platform].each do |automation|
|
|
394
|
+
context "with rspec and #{automation} without login reuse" do
|
|
395
|
+
it 'does not mention session_state' do
|
|
396
|
+
expect(read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'helpers/spec_helper.rb')).not_to include('session_state')
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
222
400
|
end
|