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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +18 -29
  3. data/.github/workflows/security.yml +68 -0
  4. data/.rubocop.yml +28 -0
  5. data/README.md +23 -4
  6. data/docs/superpowers/plans/2026-10-25-failure-video-replay.md +400 -0
  7. data/docs/superpowers/plans/2026-10-26-parity-cleanup.md +233 -0
  8. data/docs/superpowers/plans/2026-10-28-failure-card.md +295 -0
  9. data/docs/superpowers/specs/2026-10-25-failure-video-replay-design.md +214 -0
  10. data/docs/superpowers/specs/2026-10-28-failure-card-design.md +210 -0
  11. data/lib/commands/scaffolding_commands.rb +3 -3
  12. data/lib/commands/utility_commands.rb +36 -6
  13. data/lib/generators/cucumber/templates/cucumber.tt +1 -1
  14. data/lib/generators/cucumber/templates/feature.tt +5 -0
  15. data/lib/generators/cucumber/templates/partials/appium_env.tt +6 -0
  16. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -0
  17. data/lib/generators/cucumber/templates/partials/watir_env.tt +22 -0
  18. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
  19. data/lib/generators/helper_generator.rb +12 -1
  20. data/lib/generators/infrastructure/azure_generator.rb +1 -0
  21. data/lib/generators/infrastructure/github_generator.rb +1 -0
  22. data/lib/generators/infrastructure/templates/azure_pipelines.tt +12 -1
  23. data/lib/generators/infrastructure/templates/github.tt +12 -0
  24. data/lib/generators/infrastructure/templates/github_appium.tt +23 -0
  25. data/lib/generators/invoke_generators.rb +20 -2
  26. data/lib/generators/rspec/rspec_generator.rb +0 -1
  27. data/lib/generators/rspec/templates/spec.tt +12 -0
  28. data/lib/generators/templates/common/gemfile.tt +1 -1
  29. data/lib/generators/templates/common/git_ignore.tt +7 -1
  30. data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
  31. data/lib/generators/templates/common/partials/web_config.tt +9 -1
  32. data/lib/generators/templates/common/read_me.tt +84 -2
  33. data/lib/generators/templates/helpers/browser_helper.tt +5 -1
  34. data/lib/generators/templates/helpers/driver_helper.tt +1 -0
  35. data/lib/generators/templates/helpers/partials/selenium_driver.tt +4 -1
  36. data/lib/generators/templates/helpers/session_state.tt +206 -0
  37. data/lib/generators/templates/helpers/spec_helper.tt +28 -0
  38. data/lib/generators/templates/helpers/video_helper.tt +338 -171
  39. data/lib/ruby_raider/ffmpeg.rb +280 -0
  40. data/lib/ruby_raider.rb +2 -3
  41. data/lib/scaffolding/project_detector.rb +1 -3
  42. data/lib/scaffolding/scaffolding.rb +0 -1
  43. data/lib/utilities/utilities.rb +15 -2
  44. data/lib/version +1 -1
  45. data/ruby_raider.gemspec +4 -1
  46. data/sig/commands/utility_commands.rbs +4 -1
  47. data/sig/generators/invoke_generators.rbs +2 -3
  48. data/sig/ruby_raider/ffmpeg.rbs +23 -0
  49. data/sig/utilities/utilities.rbs +1 -0
  50. data/spec/commands/raider_commands_spec.rb +56 -1
  51. data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +28 -0
  52. data/spec/generators/video_helper_spec.rb +608 -0
  53. data/spec/integration/commands/browser_update_after_creation_spec.rb +7 -6
  54. data/spec/integration/commands/utility_commands_spec.rb +2 -2
  55. data/spec/integration/content/azure_content_spec.rb +33 -0
  56. data/spec/integration/content/ci_content_spec.rb +54 -0
  57. data/spec/integration/content/common_content_spec.rb +35 -0
  58. data/spec/integration/content/config_content_spec.rb +32 -0
  59. data/spec/integration/content/cucumber_reporter_content_spec.rb +47 -1
  60. data/spec/integration/content/gemfile_content_spec.rb +2 -3
  61. data/spec/integration/content/helper_content_spec.rb +178 -0
  62. data/spec/integration/content/page_content_spec.rb +0 -1
  63. data/spec/integration/content/readme_content_spec.rb +79 -0
  64. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  65. data/spec/integration/content/syntax_validation_spec.rb +15 -0
  66. data/spec/integration/content/test_content_spec.rb +27 -3
  67. data/spec/integration/end_to_end_features_spec.rb +0 -2
  68. data/spec/integration/end_to_end_spec.rb +275 -26
  69. data/spec/integration/generators/azure_generator_spec.rb +7 -6
  70. data/spec/integration/generators/common_generator_spec.rb +1 -1
  71. data/spec/integration/generators/helpers_generator_spec.rb +22 -0
  72. data/spec/integration/generators/invoke_generators_spec.rb +52 -0
  73. data/spec/integration/helpers/session_state_spec.rb +428 -0
  74. data/spec/integration/scaffolding_e2e_spec.rb +13 -24
  75. data/spec/scaffolding/scaffold_project_detector_spec.rb +0 -1
  76. data/spec/scaffolding/scaffolding_features_spec.rb +0 -1
  77. data/spec/system/selenium_spec.rb +0 -1
  78. data/spec/system/watir_spec.rb +0 -1
  79. data/spec/utilities/ffmpeg_spec.rb +424 -0
  80. data/spec/utilities/utilities_spec.rb +19 -2
  81. data/spec/utilities/video_command_spec.rb +120 -0
  82. metadata +65 -11
  83. data/lib/commands/adopt_commands.rb +0 -42
  84. data/lib/commands/loaded_commands.rb +0 -9
  85. data/lib/commands/plugin_commands.rb +0 -32
  86. data/sig/commands/adopt_commands.rbs +0 -8
  87. data/sig/commands/loaded_commands.rbs +0 -5
  88. 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.9'")
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.9'")
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
@@ -166,5 +166,4 @@ describe 'Page object content' do
166
166
  include_examples 'valid abstract page', name
167
167
  include_examples 'watir abstract page', name
168
168
  end
169
-
170
169
  end