ruby_raider 3.9.0 → 3.10.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 (81) 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 +22 -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/partials/appium_env.tt +6 -0
  15. data/lib/generators/cucumber/templates/partials/selenium_env.tt +6 -0
  16. data/lib/generators/cucumber/templates/partials/watir_env.tt +6 -0
  17. data/lib/generators/helper_generator.rb +5 -1
  18. data/lib/generators/infrastructure/azure_generator.rb +1 -0
  19. data/lib/generators/infrastructure/github_generator.rb +1 -0
  20. data/lib/generators/infrastructure/templates/azure_pipelines.tt +12 -1
  21. data/lib/generators/infrastructure/templates/github.tt +12 -0
  22. data/lib/generators/infrastructure/templates/github_appium.tt +23 -0
  23. data/lib/generators/invoke_generators.rb +20 -2
  24. data/lib/generators/rspec/rspec_generator.rb +0 -1
  25. data/lib/generators/templates/common/gemfile.tt +1 -1
  26. data/lib/generators/templates/common/git_ignore.tt +3 -0
  27. data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
  28. data/lib/generators/templates/common/partials/web_config.tt +6 -1
  29. data/lib/generators/templates/common/read_me.tt +60 -2
  30. data/lib/generators/templates/helpers/browser_helper.tt +5 -1
  31. data/lib/generators/templates/helpers/driver_helper.tt +1 -0
  32. data/lib/generators/templates/helpers/partials/selenium_driver.tt +4 -1
  33. data/lib/generators/templates/helpers/spec_helper.tt +8 -0
  34. data/lib/generators/templates/helpers/video_helper.tt +338 -171
  35. data/lib/ruby_raider/ffmpeg.rb +280 -0
  36. data/lib/ruby_raider.rb +2 -3
  37. data/lib/scaffolding/project_detector.rb +1 -3
  38. data/lib/scaffolding/scaffolding.rb +0 -1
  39. data/lib/utilities/utilities.rb +15 -2
  40. data/lib/version +1 -1
  41. data/ruby_raider.gemspec +3 -1
  42. data/sig/commands/utility_commands.rbs +4 -1
  43. data/sig/generators/invoke_generators.rbs +2 -3
  44. data/sig/ruby_raider/ffmpeg.rbs +23 -0
  45. data/sig/utilities/utilities.rbs +1 -0
  46. data/spec/commands/raider_commands_spec.rb +56 -1
  47. data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +28 -0
  48. data/spec/generators/video_helper_spec.rb +608 -0
  49. data/spec/integration/commands/browser_update_after_creation_spec.rb +7 -6
  50. data/spec/integration/commands/utility_commands_spec.rb +2 -2
  51. data/spec/integration/content/azure_content_spec.rb +29 -0
  52. data/spec/integration/content/ci_content_spec.rb +43 -0
  53. data/spec/integration/content/common_content_spec.rb +8 -0
  54. data/spec/integration/content/config_content_spec.rb +14 -0
  55. data/spec/integration/content/cucumber_reporter_content_spec.rb +22 -1
  56. data/spec/integration/content/gemfile_content_spec.rb +2 -3
  57. data/spec/integration/content/helper_content_spec.rb +146 -0
  58. data/spec/integration/content/page_content_spec.rb +0 -1
  59. data/spec/integration/content/readme_content_spec.rb +51 -0
  60. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  61. data/spec/integration/content/test_content_spec.rb +2 -3
  62. data/spec/integration/end_to_end_features_spec.rb +0 -2
  63. data/spec/integration/end_to_end_spec.rb +252 -26
  64. data/spec/integration/generators/azure_generator_spec.rb +7 -6
  65. data/spec/integration/generators/common_generator_spec.rb +1 -1
  66. data/spec/integration/generators/invoke_generators_spec.rb +52 -0
  67. data/spec/integration/scaffolding_e2e_spec.rb +13 -24
  68. data/spec/scaffolding/scaffold_project_detector_spec.rb +0 -1
  69. data/spec/scaffolding/scaffolding_features_spec.rb +0 -1
  70. data/spec/system/selenium_spec.rb +0 -1
  71. data/spec/system/watir_spec.rb +0 -1
  72. data/spec/utilities/ffmpeg_spec.rb +424 -0
  73. data/spec/utilities/utilities_spec.rb +19 -2
  74. data/spec/utilities/video_command_spec.rb +120 -0
  75. metadata +49 -11
  76. data/lib/commands/adopt_commands.rb +0 -42
  77. data/lib/commands/loaded_commands.rb +0 -9
  78. data/lib/commands/plugin_commands.rb +0 -32
  79. data/sig/commands/adopt_commands.rbs +0 -8
  80. data/sig/commands/loaded_commands.rbs +0 -5
  81. data/sig/commands/plugin_commands.rbs +0 -9
@@ -4,6 +4,8 @@ require_relative 'spec_helper'
4
4
  require 'timeout'
5
5
  require 'open3'
6
6
  require 'json'
7
+ require 'erb'
8
+ require 'tmpdir'
7
9
 
8
10
  # End-to-end integration tests that verify generated projects actually work
9
11
  # These tests:
@@ -15,7 +17,7 @@ require 'json'
15
17
  # This ensures the template system produces working, executable test frameworks
16
18
  describe 'End-to-End Project Generation and Execution' do
17
19
  # Helper to run commands in a generated project directory
18
- def run_in_project(project_name, command, timeout: 300)
20
+ def run_in_project(project_name, command, timeout: 300, env: {})
19
21
  result = {
20
22
  success: false,
21
23
  stdout: '',
@@ -26,7 +28,7 @@ describe 'End-to-End Project Generation and Execution' do
26
28
  Dir.chdir(project_name) do
27
29
  Bundler.with_unbundled_env do
28
30
  Timeout.timeout(timeout) do
29
- stdout, stderr, status = Open3.capture3(command)
31
+ stdout, stderr, status = Open3.capture3(env, command)
30
32
  result[:stdout] = stdout
31
33
  result[:stderr] = stderr
32
34
  result[:exit_code] = status.exitstatus
@@ -49,6 +51,15 @@ describe 'End-to-End Project Generation and Execution' do
49
51
  gemfile = File.join(project_name, 'Gemfile')
50
52
  root = File.expand_path('../..', __dir__)
51
53
  File.write(gemfile, File.read(gemfile).sub(/^gem 'ruby_raider'.*$/, "gem 'ruby_raider', path: '#{root}'"))
54
+ use_local_raider_reporter(gemfile)
55
+ end
56
+
57
+ # Set RAIDER_REPORTER_PATH to a raider_reporter checkout to resolve a release that is not on RubyGems yet.
58
+ def use_local_raider_reporter(gemfile)
59
+ path = ENV.fetch('RAIDER_REPORTER_PATH', nil)
60
+ return unless path
61
+
62
+ File.write(gemfile, File.read(gemfile).sub(/^gem 'raider_reporter'.*$/, "gem 'raider_reporter', path: '#{path}'"))
52
63
  end
53
64
 
54
65
  # Helper to install dependencies
@@ -104,10 +115,10 @@ describe 'End-to-End Project Generation and Execution' do
104
115
  expect(bundle_install(project)).to be true
105
116
  end
106
117
 
107
- it 'resolves raider_reporter 0.9 from RubyGems and ships the raider-reporter executable', :aggregate_failures do
118
+ it 'resolves raider_reporter 0.10+ from RubyGems and ships the raider-reporter executable', :aggregate_failures do
108
119
  skip 'Bundle install failed' unless bundle_install(project)
109
120
 
110
- expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.9\.\d+\)$/)
121
+ expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.1\d\.\d+\)$/)
111
122
  result = run_in_project(project, 'bundle exec raider-reporter --help', timeout: 60)
112
123
  expect(result[:exit_code]).to eq(0), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
113
124
  expect(result[:stdout] + result[:stderr]).to include('raider-reporter comment')
@@ -163,11 +174,11 @@ describe 'End-to-End Project Generation and Execution' do
163
174
  "STDERR: #{result[:stderr]}"
164
175
  end
165
176
 
166
- it 'resolves raider_reporter 0.9 from RubyGems and requires its Cucumber plugin', :aggregate_failures do
177
+ it 'resolves raider_reporter 0.10+ from RubyGems and requires its Cucumber plugin', :aggregate_failures do
167
178
  skip 'Bundle install failed' unless bundle_install(project)
168
179
 
169
180
  lockfile = File.read(File.join(project, 'Gemfile.lock'))
170
- expect(lockfile).to match(/^ raider_reporter \(0\.9\.\d+\)$/)
181
+ expect(lockfile).to match(/^ raider_reporter \(0\.1\d\.\d+\)$/)
171
182
  expect(File.read(File.join(project, 'features/support/env.rb'))).to include("require 'raider_reporter/cucumber'")
172
183
  end
173
184
 
@@ -182,6 +193,17 @@ describe 'End-to-End Project Generation and Execution' do
182
193
  expect(result[:exit_code]).to eq(1)
183
194
  end
184
195
 
196
+ it 'writes rerun.txt listing only the failing scenario', :aggregate_failures, :slow do
197
+ skip 'Bundle install failed' unless bundle_install(project)
198
+
199
+ result = run_cucumber_with_failure(project)
200
+
201
+ expect(result[:exit_code]).to eq(1)
202
+ rerun = File.join(project, 'rerun.txt')
203
+ expect(File).to exist(rerun)
204
+ expect(File.read(rerun).strip).to eq('features/reporter_failure.feature:3')
205
+ end
206
+
185
207
  it 'writes raider-results/run.json listing every scenario with a failing scenario screenshot',
186
208
  :aggregate_failures, :slow do
187
209
  skip 'Bundle install failed' unless bundle_install(project)
@@ -211,7 +233,7 @@ describe 'End-to-End Project Generation and Execution' do
211
233
  # Runs the project's features plus a deliberately failing scenario, then removes that scenario again
212
234
  def run_cucumber_with_failure(project)
213
235
  add_failing_cucumber_scenario(project)
214
- FileUtils.rm_rf(File.join(project, 'raider-results'))
236
+ FileUtils.rm_rf([File.join(project, 'raider-results'), File.join(project, 'rerun.txt')])
215
237
  run_cucumber(project)
216
238
  ensure
217
239
  remove_failing_cucumber_scenario(project)
@@ -263,20 +285,22 @@ describe 'End-to-End Project Generation and Execution' do
263
285
  end
264
286
 
265
287
  # Recorded Azure Pipelines env of a push (IndividualCI) build, see raider_reporter's CiDetector specs
266
- AZURE_PUSH_ENV = {
267
- 'TF_BUILD' => 'True',
268
- 'SYSTEM_COLLECTIONURI' => 'https://dev.azure.com/raider-test/',
269
- 'SYSTEM_TEAMPROJECT' => 'My Project',
270
- 'SYSTEM_TEAMPROJECTID' => '6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c',
271
- 'BUILD_REPOSITORY_NAME' => 'web tests',
272
- 'BUILD_REPOSITORY_ID' => '1c2bd1a0-3f63-4d93-9c3c-2b4f2b1d8f11',
273
- 'BUILD_REPOSITORY_PROVIDER' => 'TfsGit',
274
- 'BUILD_BUILDID' => '42',
275
- 'SYSTEM_JOBDISPLAYNAME' => 'tests',
276
- 'BUILD_REASON' => 'IndividualCI',
277
- 'BUILD_SOURCEBRANCH' => 'refs/heads/release/1.2',
278
- 'BUILD_SOURCEVERSION' => 'abc'
279
- }.freeze
288
+ def azure_push_env
289
+ {
290
+ 'TF_BUILD' => 'True',
291
+ 'SYSTEM_COLLECTIONURI' => 'https://dev.azure.com/raider-test/',
292
+ 'SYSTEM_TEAMPROJECT' => 'My Project',
293
+ 'SYSTEM_TEAMPROJECTID' => '6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c',
294
+ 'BUILD_REPOSITORY_NAME' => 'web tests',
295
+ 'BUILD_REPOSITORY_ID' => '1c2bd1a0-3f63-4d93-9c3c-2b4f2b1d8f11',
296
+ 'BUILD_REPOSITORY_PROVIDER' => 'TfsGit',
297
+ 'BUILD_BUILDID' => '42',
298
+ 'SYSTEM_JOBDISPLAYNAME' => 'tests',
299
+ 'BUILD_REASON' => 'IndividualCI',
300
+ 'BUILD_SOURCEBRANCH' => 'refs/heads/release/1.2',
301
+ 'BUILD_SOURCEVERSION' => 'abc'
302
+ }.freeze
303
+ end
280
304
 
281
305
  # Generates a ci: azure web project (the shared before(:all) only builds the default github ones)
282
306
  def generate_azure_project(framework)
@@ -289,7 +313,7 @@ describe 'End-to-End Project Generation and Execution' do
289
313
  end
290
314
 
291
315
  def azure_env_prefix
292
- "GITHUB_ACTIONS= HEADLESS=true #{AZURE_PUSH_ENV.map { |k, v| "#{k}='#{v}'" }.join(' ')}"
316
+ "GITHUB_ACTIONS= HEADLESS=true #{azure_push_env.map { |k, v| "#{k}='#{v}'" }.join(' ')}"
293
317
  end
294
318
 
295
319
  def expect_azure_ci_block(project)
@@ -310,12 +334,12 @@ describe 'End-to-End Project Generation and Execution' do
310
334
  describe 'Selenium + RSpec' do
311
335
  let(:project) { generate_azure_project('rspec') }
312
336
 
313
- it 'ships azure-pipelines.yml and resolves raider_reporter 0.9', :aggregate_failures do
337
+ it 'ships azure-pipelines.yml and resolves raider_reporter 0.10+', :aggregate_failures do
314
338
  expect(File).to exist(File.join(project, 'azure-pipelines.yml'))
315
339
  expect(File).not_to exist(File.join(project, '.github/workflows'))
316
340
  skip 'Bundle install failed' unless bundle_install(project)
317
341
 
318
- expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.9\.\d+\)$/)
342
+ expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.1\d\.\d+\)$/)
319
343
  end
320
344
 
321
345
  it 'records provider azure_pipelines and an azure_pipelines: build key in run.json', :aggregate_failures, :slow do
@@ -332,11 +356,11 @@ describe 'End-to-End Project Generation and Execution' do
332
356
  describe 'Selenium + Cucumber' do
333
357
  let(:project) { generate_azure_project('cucumber') }
334
358
 
335
- it 'ships azure-pipelines.yml and resolves raider_reporter 0.9', :aggregate_failures do
359
+ it 'ships azure-pipelines.yml and resolves raider_reporter 0.10+', :aggregate_failures do
336
360
  expect(File).to exist(File.join(project, 'azure-pipelines.yml'))
337
361
  skip 'Bundle install failed' unless bundle_install(project)
338
362
 
339
- expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.9\.\d+\)$/)
363
+ expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.1\d\.\d+\)$/)
340
364
  end
341
365
 
342
366
  it 'records provider azure_pipelines and an azure_pipelines: build key in run.json', :aggregate_failures, :slow do
@@ -351,6 +375,208 @@ describe 'End-to-End Project Generation and Execution' do
351
375
  end
352
376
  end
353
377
 
378
+ # Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH), because the
379
+ # RaiderHQ ffmpeg release has no checksums yet. Skipped when it, or the browser, is missing. Firefox can be
380
+ # pointed at a specific binary with FIREFOX_BINARY.
381
+ context 'Failure video replay', :slow, :video do
382
+ let(:ffmpeg_path) { ENV.fetch('RAIDER_FFMPEG_PATH', '') }
383
+ let(:cache_dir) { Dir.mktmpdir('raider-cache') }
384
+
385
+ after { FileUtils.rm_rf(cache_dir) }
386
+
387
+ def skip_unless_video_prerequisites(browser)
388
+ skip 'RAIDER_FFMPEG_PATH is not set to an executable ffmpeg with libvpx' unless
389
+ File.file?(ffmpeg_path) && File.executable?(ffmpeg_path)
390
+ skip "#{browser} is not installed" unless browser_installed?(browser)
391
+ end
392
+
393
+ def browser_installed?(browser)
394
+ if browser == 'firefox'
395
+ ENV['FIREFOX_BINARY'] ? File.executable?(ENV['FIREFOX_BINARY']) : app_or_command?('Firefox', 'firefox')
396
+ else
397
+ app_or_command?('Google Chrome', 'google-chrome') || app_or_command?('Chromium', 'chromium')
398
+ end
399
+ end
400
+
401
+ def app_or_command?(app, command)
402
+ File.directory?("/Applications/#{app}.app") || system("command -v #{command} > /dev/null 2>&1")
403
+ end
404
+
405
+ # A separate project, so the default projects keep resolving raider_reporter 0.10+ from RubyGems
406
+ def generate_video_project(framework, automation)
407
+ name = "video_#{framework}_#{automation}"
408
+ unless LazyProjectGenerator::GENERATED.include?(name)
409
+ generate_framework(framework:, automation:, name:)
410
+ LazyProjectGenerator::GENERATED.add(name)
411
+ use_local_ruby_raider(name)
412
+ expect(run_in_project(name, 'bundle install --quiet', timeout: 300)[:success]).to be(true)
413
+ end
414
+ FileUtils.rm_rf(File.join(name, 'raider-results'))
415
+ name
416
+ end
417
+
418
+ def video_env(overrides = {})
419
+ { 'HEADLESS' => 'true', 'RAIDER_FFMPEG_PATH' => ffmpeg_path, 'RAIDER_CACHE_DIR' => cache_dir,
420
+ 'RAIDER_TOKEN' => nil, 'RAIDER_ENDPOINT' => nil, 'VIDEO' => nil }.merge(overrides)
421
+ end
422
+
423
+ def page_url
424
+ "data:text/html,#{ERB::Util.url_encode('<h1 id="title">Video</h1><input id="name"><button id="go">Go</button>')}"
425
+ end
426
+
427
+ def add_rspec_video_spec(project)
428
+ File.write(File.join(project, 'spec/video_replay_spec.rb'), <<~SPEC)
429
+ # frozen_string_literal: true
430
+
431
+ require_relative '../helpers/spec_helper'
432
+
433
+ describe 'Video replay' do
434
+ it 'passes after driving the page' do
435
+ driver.navigate.to('#{page_url}')
436
+ driver.find_element(id: 'name').send_keys('ok')
437
+ expect(driver.find_element(id: 'title').text).to eq('Video')
438
+ end
439
+
440
+ it 'fails after driving the page' do
441
+ driver.navigate.to('#{page_url}')
442
+ driver.find_element(id: 'name').send_keys('broken')
443
+ driver.find_element(id: 'go').click
444
+ expect(driver.find_element(id: 'title').text).to eq('deliberate video failure')
445
+ end
446
+ end
447
+ SPEC
448
+ end
449
+
450
+ def add_cucumber_video_feature(project, firefox_binary)
451
+ File.write(File.join(project, 'features/video_replay.feature'), <<~FEATURE)
452
+ Feature: Video replay
453
+
454
+ Scenario: A passing scenario
455
+ Given the video page is open
456
+ Then the video page title is "Video"
457
+
458
+ Scenario: A failing scenario
459
+ Given the video page is open
460
+ Then the video page title is "deliberate video failure"
461
+ FEATURE
462
+ File.write(File.join(project, 'features/step_definitions/video_replay_steps.rb'), <<~STEPS)
463
+ # frozen_string_literal: true
464
+
465
+ Given('the video page is open') do
466
+ browser.goto('#{page_url}')
467
+ browser.text_field(id: 'name').set('typed')
468
+ browser.button(id: 'go').click
469
+ end
470
+
471
+ Then('the video page title is {string}') do |title|
472
+ raise "expected title \#{title}" unless browser.h1(id: 'title').text == title
473
+ end
474
+ STEPS
475
+ config = File.join(project, 'config/config.yml')
476
+ File.write(config, File.read(config).sub(/^browser: .*$/, 'browser: firefox'))
477
+ # Test setup only: the generated templates do not read FIREFOX_BINARY.
478
+ File.write(File.join(project, 'features/support/zz_firefox_binary.rb'), <<~RUBY) if firefox_binary
479
+ # frozen_string_literal: true
480
+
481
+ Selenium::WebDriver::Firefox.path = ENV['FIREFOX_BINARY'] if ENV['FIREFOX_BINARY']
482
+ RUBY
483
+ end
484
+
485
+ def results(project)
486
+ JSON.parse(File.read(File.join(project, 'raider-results', 'run.json')))['results']
487
+ end
488
+
489
+ def video_files(project)
490
+ Dir.glob(File.join(project, 'raider-results', '**', '*.{webm,mp4}'))
491
+ end
492
+
493
+ def video_attachments(result)
494
+ result['attachments'].select { |attachment| attachment['name'] == 'video' }
495
+ end
496
+
497
+ def expect_video_for_failure_only(project, failing_name, run)
498
+ by_status = results(project).group_by { |r| r['status'] }
499
+ expect(by_status.keys).to match_array(%w[passed failed]), "STDOUT: #{run[:stdout]}\nSTDERR: #{run[:stderr]}"
500
+ failing = by_status['failed'].find { |r| r['full_name'].include?(failing_name) }
501
+ expect(failing).not_to be_nil, "No failing result named #{failing_name}"
502
+ expect(video_attachments(failing).size).to eq(1)
503
+ expect(video_attachments(failing).first['content_type']).to eq('video/webm')
504
+ by_status.fetch('passed', []).each { |r| expect(video_attachments(r)).to be_empty }
505
+ videos = video_files(project)
506
+ expect(videos.size).to eq(1)
507
+ expect(videos.first).to end_with('video.webm')
508
+ expect(File.size(videos.first)).to be_positive
509
+ expect(File.read(File.join(project, 'raider-results', 'index.html'))).to include('<video')
510
+ end
511
+
512
+ describe 'Selenium + RSpec on headless Chrome' do
513
+ let(:project) { 'video_rspec_selenium' }
514
+ let(:command) { 'bundle exec rspec spec/video_replay_spec.rb' }
515
+
516
+ before do
517
+ skip_unless_video_prerequisites('chrome')
518
+ generate_video_project('rspec', 'selenium')
519
+ add_rspec_video_spec(project)
520
+ end
521
+
522
+ after { FileUtils.rm_f(File.join(project, 'spec/video_replay_spec.rb')) }
523
+
524
+ it 'attaches video.webm to the failing example only and plays it in index.html', :aggregate_failures do
525
+ result = run_in_project(project, command, env: video_env)
526
+ expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
527
+ expect(result[:stderr]).not_to include('[video]')
528
+ expect_video_for_failure_only(project, 'fails after driving the page', result)
529
+ end
530
+
531
+ it 'writes no video with VIDEO=off', :aggregate_failures do
532
+ result = run_in_project(project, command, env: video_env('VIDEO' => 'off'))
533
+ expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
534
+ expect(video_files(project)).to be_empty
535
+ expect(results(project).flat_map { |r| video_attachments(r) }).to be_empty
536
+ expect(result[:stderr]).not_to include('[video]')
537
+ end
538
+
539
+ it 'runs unchanged with one [video] line and no download when no ffmpeg is usable', :aggregate_failures do
540
+ off = run_in_project(project, command, env: video_env('VIDEO' => 'off'))
541
+ FileUtils.rm_rf(File.join(project, 'raider-results'))
542
+ result = run_in_project(project, command, env: video_env('RAIDER_FFMPEG_PATH' => nil))
543
+
544
+ expect(result[:exit_code]).to eq(off[:exit_code]), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
545
+ video_lines = result[:stderr].lines.grep(/\[video\]/)
546
+ expect(video_lines.size).to eq(1), "STDERR: #{result[:stderr]}"
547
+ # Decided at the first driver/recorder call, before any capture or fetch: the placeholder
548
+ # manifest has no checksum, so nothing is captured or downloaded.
549
+ expect(video_lines.first).to include('no ffmpeg available for this platform yet')
550
+ expect(video_files(project)).to be_empty
551
+ expect(Dir.glob(File.join(cache_dir, '**', '*'))).to be_empty
552
+ end
553
+ end
554
+
555
+ describe 'Watir + Cucumber on headless Firefox' do
556
+ let(:project) { 'video_cucumber_watir' }
557
+ let(:firefox_binary) { ENV.fetch('FIREFOX_BINARY', nil) }
558
+
559
+ before do
560
+ skip_unless_video_prerequisites('firefox')
561
+ generate_video_project('cucumber', 'watir')
562
+ add_cucumber_video_feature(project, firefox_binary)
563
+ end
564
+
565
+ after do
566
+ FileUtils.rm_f(File.join(project, 'features/video_replay.feature'))
567
+ FileUtils.rm_f(File.join(project, 'features/step_definitions/video_replay_steps.rb'))
568
+ end
569
+
570
+ it 'attaches video.webm to the failing scenario only and plays it in index.html', :aggregate_failures do
571
+ result = run_in_project(project, 'bundle exec cucumber features/video_replay.feature',
572
+ env: video_env('FIREFOX_BINARY' => firefox_binary), timeout: 240)
573
+ expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
574
+ expect(result[:stderr]).not_to include('[video]')
575
+ expect_video_for_failure_only(project, 'A failing scenario', result)
576
+ end
577
+ end
578
+ end
579
+
354
580
  # Mobile and Visual frameworks require external services, so we only verify structure
355
581
  context 'Mobile Automation Frameworks (structure validation only)' do
356
582
  describe 'iOS + RSpec' do
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'open3'
4
5
  require 'yaml'
5
6
  require_relative '../../../lib/generators/infrastructure/azure_generator'
6
7
  require_relative '../spec_helper'
@@ -63,10 +64,10 @@ describe AzureGenerator do
63
64
  end
64
65
 
65
66
  context 'with an invalid ci' do
66
- it 'raises an ArgumentError and generates nothing' do
67
+ it 'raises a Thor::Error and generates nothing' do
67
68
  expect do
68
69
  InvokeGenerators.generate_framework(automation: 'selenium', framework: 'rspec', name: 'azure_bad', ci: 'gitlab')
69
- end.to raise_error(ArgumentError, 'ci must be github or azure')
70
+ end.to raise_error(Thor::Error, 'ci must be github or azure')
70
71
  expect(File).not_to exist('azure_bad')
71
72
  end
72
73
  end
@@ -74,10 +75,10 @@ describe AzureGenerator do
74
75
  context 'with raider new and ci:gitlab' do
75
76
  it 'prints the error, generates nothing and exits non-zero' do
76
77
  root = File.expand_path('../../..', __dir__)
77
- out = IO.popen(['ruby', "#{root}/bin/raider", 'new', 'azure_cli_bad', '-p', 'framework:rspec',
78
- 'automation:selenium', 'ci:gitlab', { err: %i[child out] }], &:read)
79
- expect($CHILD_STATUS.exitstatus).not_to eq(0)
80
- expect(out).to include('Invalid parameters: ci must be github or azure')
78
+ out, status = Open3.capture2e('ruby', "#{root}/bin/raider", 'new', 'azure_cli_bad', '-p', 'framework:rspec',
79
+ 'automation:selenium', 'ci:gitlab')
80
+ expect(status.exitstatus).not_to eq(0)
81
+ expect(out).to include('ci must be github or azure')
81
82
  expect(File).not_to exist('azure_cli_bad')
82
83
  end
83
84
  end
@@ -18,7 +18,7 @@ describe CommonGenerator do
18
18
  end
19
19
  end
20
20
 
21
- shared_examples 'creates a config file' do |name|
21
+ shared_examples 'creates a config file' do |name|
22
22
  it 'creates a config file' do
23
23
  expect(File).to exist("#{name}/config/config.yml")
24
24
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+ require_relative '../spec_helper'
6
+
7
+ describe InvokeGenerators do
8
+ after { FileUtils.rm_rf(Dir.glob('case_*')) }
9
+
10
+ context 'with option values in a different case' do
11
+ it 'accepts framework:Cucumber and automation:Selenium' do
12
+ described_class.generate_framework(framework: 'Cucumber', automation: 'Selenium', name: 'case_mixed')
13
+ expect(File).to exist('case_mixed/cucumber.yml')
14
+ end
15
+
16
+ it 'accepts upper-case values' do
17
+ described_class.generate_framework(framework: 'RSPEC', automation: 'WATIR', name: 'case_upper')
18
+ expect(File).to exist('case_upper/.rspec')
19
+ end
20
+
21
+ it 'accepts symbols' do
22
+ described_class.generate_framework(framework: :rspec, automation: :selenium, name: 'case_symbol')
23
+ expect(File).to exist('case_symbol/.rspec')
24
+ end
25
+ end
26
+
27
+ context 'with raider new and capitalised options' do
28
+ it 'generates the project and exits zero' do
29
+ root = File.expand_path('../../..', __dir__)
30
+ _out, status = Open3.capture2e('ruby', "#{root}/bin/raider", 'new', 'case_cli', '-p', 'framework:Cucumber',
31
+ 'automation:Selenium')
32
+ expect(status.exitstatus).to eq(0)
33
+ expect(File).to exist('case_cli/cucumber.yml')
34
+ end
35
+ end
36
+
37
+ context 'with invalid values' do
38
+ it 'raises a Thor::Error for an unknown framework' do
39
+ expect { described_class.generate_framework(framework: 'minitest', automation: 'selenium', name: 'case_bad') }
40
+ .to raise_error(Thor::Error, /Invalid framework "minitest"/)
41
+ end
42
+
43
+ it 'sends an invalid ci to stderr and exits non-zero' do
44
+ root = File.expand_path('../../..', __dir__)
45
+ _out, err, status = Open3.capture3('ruby', "#{root}/bin/raider", 'new', 'case_bad_ci', '-p',
46
+ 'framework:rspec', 'automation:selenium', 'ci:gitlab')
47
+ expect(status.exitstatus).to eq(1)
48
+ expect(err).to include('ci must be github or azure')
49
+ expect(File).not_to exist('case_bad_ci')
50
+ end
51
+ end
52
+ end
@@ -23,11 +23,7 @@ end
23
23
 
24
24
  # End-to-end scaffolding tests that verify the scaffold command creates the right
25
25
  # files with the right content across all automation × framework combinations.
26
- describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
27
-
28
- WEB_AUTOMATIONS = %w[selenium watir].freeze
29
- TEST_FRAMEWORKS = %w[rspec cucumber].freeze
30
-
26
+ describe 'Scaffolding E2E' do
31
27
  def self.scaffold_style(framework)
32
28
  case framework
33
29
  when 'rspec' then :spec
@@ -36,7 +32,7 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
36
32
  end
37
33
 
38
34
  # Build a minimal mock project that the scaffolding system can detect.
39
- def self.build_mock_project(automation, framework) # rubocop:disable Metrics/MethodLength
35
+ def self.build_mock_project(automation, framework)
40
36
  dir = File.expand_path("tmp_scaffold_e2e_#{framework}_#{automation}")
41
37
  FileUtils.rm_rf(dir)
42
38
  FileUtils.mkdir_p("#{dir}/page_objects/pages")
@@ -61,7 +57,7 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
61
57
  when 'rspec' then gems << "gem 'rspec'"
62
58
  when 'cucumber' then gems << "gem 'cucumber'"
63
59
  end
64
- File.write("#{dir}/Gemfile", gems.join("\n") + "\n")
60
+ File.write("#{dir}/Gemfile", "#{gems.join("\n")}\n")
65
61
  File.write("#{dir}/config/config.yml", "browser: chrome\nurl: http://localhost:3000\n")
66
62
  File.write("#{dir}/page_objects/abstract/page.rb", "class Page; end\n")
67
63
  File.write("#{dir}/page_objects/abstract/component.rb", "class Component; end\n")
@@ -91,7 +87,7 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
91
87
  end
92
88
 
93
89
  it 'includes url method (config has url)' do
94
- expect(page_content).to include("def url(_page)")
90
+ expect(page_content).to include('def url(_page)')
95
91
  expect(page_content).to include("'checkout'")
96
92
  end
97
93
  end
@@ -326,11 +322,11 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
326
322
  # Test matrix: automation × framework
327
323
  # ══════════════════════════════════════════════
328
324
 
329
- WEB_AUTOMATIONS.each do |automation|
330
- TEST_FRAMEWORKS.each do |framework|
325
+ %w[selenium watir].each do |automation|
326
+ %w[rspec cucumber].each do |framework|
331
327
  style = scaffold_style(framework)
332
328
 
333
- context "with #{framework} and #{automation}" do # rubocop:disable RSpec/ContextWording
329
+ context "with #{framework} and #{automation}" do
334
330
  let(:scaffold) { ScaffoldingCommands }
335
331
 
336
332
  # Use absolute path so Dir.chdir is safe regardless of current CWD
@@ -367,6 +363,9 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
367
363
  end
368
364
  end
369
365
 
366
+ # Page content validation
367
+ let(:page_content) { File.read('page_objects/pages/checkout.rb') }
368
+
370
369
  it 'creates page object' do
371
370
  expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
372
371
  end
@@ -389,9 +388,6 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
389
388
  end
390
389
  end
391
390
 
392
- # Page content validation
393
- let(:page_content) { File.read('page_objects/pages/checkout.rb') }
394
-
395
391
  include_examples 'valid scaffolded page'
396
392
  include_examples "#{automation} scaffolded page"
397
393
 
@@ -459,16 +455,10 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
459
455
 
460
456
  it 'generates multiple scaffolds at once' do
461
457
  scaffold.new.invoke(:scaffold, nil, %w[search filter])
462
- expect(Pathname.new('page_objects/pages/search.rb')).to be_file
463
- expect(Pathname.new('page_objects/pages/filter.rb')).to be_file
464
-
465
- if style == :spec
466
- expect(Pathname.new('spec/search_page_spec.rb')).to be_file
467
- expect(Pathname.new('spec/filter_page_spec.rb')).to be_file
468
- else
469
- expect(Pathname.new('features/search.feature')).to be_file
470
- expect(Pathname.new('features/filter.feature')).to be_file
458
+ files = %w[search filter].flat_map do |name|
459
+ ["page_objects/pages/#{name}.rb", style == :spec ? "spec/#{name}_page_spec.rb" : "features/#{name}.feature"]
471
460
  end
461
+ expect(files.reject { |file| File.file?(file) }).to be_empty
472
462
  end
473
463
  end
474
464
 
@@ -496,7 +486,6 @@ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
496
486
  expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
497
487
  end
498
488
  end
499
-
500
489
  end
501
490
  end
502
491
  end
@@ -33,7 +33,6 @@ RSpec.describe ScaffoldProjectDetector do
33
33
  it 'reports selenium?' do
34
34
  expect(described_class.selenium?).to be true
35
35
  end
36
-
37
36
  end
38
37
 
39
38
  context 'with watir project' do
@@ -104,5 +104,4 @@ RSpec.describe 'Scaffolding features' do
104
104
  expect(Pathname.new('spec/dashboard_page_spec.rb')).to be_file
105
105
  end
106
106
  end
107
-
108
107
  end
@@ -35,5 +35,4 @@ describe 'Selenium based frameworks' do
35
35
  context 'with cucumber' do
36
36
  include_examples 'runs tests successfully', 'cucumber'
37
37
  end
38
-
39
38
  end
@@ -35,5 +35,4 @@ describe 'Watir based frameworks' do
35
35
  context 'with cucumber' do
36
36
  include_examples 'runs tests successfully', 'cucumber'
37
37
  end
38
-
39
38
  end