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
@@ -12,7 +12,9 @@ pool:
12
12
 
13
13
  variables:
14
14
  HEADLESS: 'true'
15
- # Add RAIDER_TOKEN (secret) and RAIDER_ENDPOINT as pipeline variables.
15
+ # Where ffmpeg for failure videos is cached between runs.
16
+ RAIDER_CACHE_DIR: $(Pipeline.Workspace)/.cache/raider
17
+ # Add RAIDER_TOKEN (secret) and RAIDER_ENDPOINT (your Raider Hideout URL) as pipeline variables.
16
18
 
17
19
  jobs:
18
20
  - job: tests
@@ -21,6 +23,15 @@ jobs:
21
23
  inputs: { versionSpec: '3.4' }
22
24
  - script: bundle install
23
25
  displayName: Install gems
26
+ - task: Cache@2
27
+ displayName: Cache ffmpeg for failure videos
28
+ inputs:
29
+ key: 'raider-ffmpeg-<%= RubyRaider::Ffmpeg::MANIFEST[:version] %> | "$(Agent.OS)" | Gemfile.lock'
30
+ path: $(RAIDER_CACHE_DIR)
31
+ # Video is optional: a failed download is visible here but never fails the build.
32
+ - script: bundle exec raider u video install
33
+ displayName: Install ffmpeg for failure videos
34
+ continueOnError: true
24
35
  - script: <%- if cucumber? -%>bundle exec raider-reporter exec -- bundle exec cucumber features/ --format pretty<%- else -%>bundle exec raider-reporter exec -- bundle exec rspec spec/ --format documentation<%- end %>
25
36
  displayName: Run tests
26
37
  env:
@@ -33,8 +33,20 @@ jobs:
33
33
  with:
34
34
  chrome-version: stable
35
35
 
36
+ - name: Cache ffmpeg for failure videos
37
+ uses: actions/cache@v4
38
+ with:
39
+ path: ~/.cache/raider
40
+ key: raider-ffmpeg-<%= RubyRaider::Ffmpeg::MANIFEST[:version] %>-${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
41
+
42
+ - name: Install ffmpeg for failure videos
43
+ run: bundle exec raider u video install
44
+ # Video is optional: a failed download is visible here but never fails the build.
45
+ continue-on-error: true
46
+
36
47
  - name: Run tests
37
48
  run: <%- if framework == 'cucumber' -%>bundle exec raider-reporter exec -- bundle exec cucumber features --format pretty<%- else -%>bundle exec raider-reporter exec -- bundle exec rspec spec --format documentation<%- end %>
49
+ # RAIDER_ENDPOINT: your Raider Hideout URL (repository variable); RAIDER_TOKEN: a project token (secret).
38
50
  env:
39
51
  HEADLESS: true
40
52
  RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
@@ -44,12 +44,24 @@ jobs:
44
44
  appium &
45
45
  sleep 5
46
46
 
47
+ - name: Cache ffmpeg for failure videos
48
+ uses: actions/cache@v4
49
+ with:
50
+ path: ~/.cache/raider
51
+ key: raider-ffmpeg-<%= RubyRaider::Ffmpeg::MANIFEST[:version] %>-${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
52
+
53
+ - name: Install ffmpeg for failure videos
54
+ run: bundle exec raider u video install
55
+ # Video is optional: a failed download is visible here but never fails the build.
56
+ continue-on-error: true
57
+
47
58
  - name: Run tests
48
59
  <%- if cucumber? -%>
49
60
  run: bundle exec raider-reporter exec -- bundle exec cucumber features/ --format pretty
50
61
  <%- else -%>
51
62
  run: bundle exec raider-reporter exec -- bundle exec rspec spec/ --format documentation
52
63
  <%- end -%>
64
+ # RAIDER_ENDPOINT: your Raider Hideout URL (repository variable); RAIDER_TOKEN: a project token (secret).
53
65
  env:
54
66
  APP_PATH: ${{ github.workspace }}/apps/RaiderTestApp.app
55
67
  RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
@@ -101,6 +113,17 @@ jobs:
101
113
  appium &
102
114
  sleep 5
103
115
 
116
+ - name: Cache ffmpeg for failure videos
117
+ uses: actions/cache@v4
118
+ with:
119
+ path: ~/.cache/raider
120
+ key: raider-ffmpeg-<%= RubyRaider::Ffmpeg::MANIFEST[:version] %>-${{ runner.os }}-${{ hashFiles('**/Gemfile.lock') }}
121
+
122
+ - name: Install ffmpeg for failure videos
123
+ run: bundle exec raider u video install
124
+ # Video is optional: a failed download is visible here but never fails the build.
125
+ continue-on-error: true
126
+
104
127
  - name: Run Android Emulator and Tests
105
128
  uses: reactivecircus/android-emulator-runner@v2
106
129
  with:
@@ -1,3 +1,4 @@
1
+ require 'thor'
1
2
  require_relative 'infrastructure/github_generator'
2
3
  require_relative 'infrastructure/azure_generator'
3
4
  require_relative 'automation/automation_generator'
@@ -7,11 +8,15 @@ require_relative 'helper_generator'
7
8
  require_relative 'rspec/rspec_generator'
8
9
 
9
10
  module InvokeGenerators
10
- class InvalidParameters < ArgumentError; end
11
+ FRAMEWORKS = %w[rspec cucumber].freeze
12
+ AUTOMATIONS = %w[selenium watir ios android cross_platform].freeze
11
13
 
12
14
  module_function
13
15
 
14
16
  def generate_framework(structure = {})
17
+ structure = normalise_options(structure)
18
+ validate_option(:framework, structure[:framework], FRAMEWORKS)
19
+ validate_option(:automation, structure[:automation], AUTOMATIONS)
15
20
  validate_ci(structure[:ci])
16
21
  generators = %w[Automation Common Helpers]
17
22
  framework = structure[:framework]
@@ -30,10 +35,23 @@ module InvokeGenerators
30
35
  print_mobile_azure_notice(structure)
31
36
  end
32
37
 
38
+ def normalise_options(structure)
39
+ structure.merge(
40
+ framework: structure[:framework].to_s.downcase,
41
+ automation: structure[:automation].to_s.downcase
42
+ )
43
+ end
44
+
45
+ def validate_option(key, value, allowed)
46
+ return if allowed.include?(value.to_s)
47
+
48
+ raise Thor::Error, "Invalid #{key} #{value.to_s.inspect}. Allowed values: #{allowed.join(', ')}"
49
+ end
50
+
33
51
  def validate_ci(provider)
34
52
  return if [nil, '', 'github', 'azure'].include?(provider&.to_s)
35
53
 
36
- raise InvalidParameters, 'ci must be github or azure'
54
+ raise Thor::Error, 'ci must be github or azure'
37
55
  end
38
56
 
39
57
  def print_mobile_azure_notice(structure)
@@ -20,5 +20,4 @@ class RspecGenerator < Generator
20
20
 
21
21
  template('accessibility_spec.tt', "#{name}/spec/accessibility_spec.rb")
22
22
  end
23
-
24
23
  end
@@ -13,7 +13,7 @@ gem 'parallel_split_test'
13
13
  gem 'parallel_tests'
14
14
  gem 'rake'
15
15
  gem 'reek'
16
- gem 'raider_reporter', '~> 0.9'
16
+ gem 'raider_reporter', '~> 0.10'
17
17
  <%- if rspec? -%>
18
18
  gem 'rspec-retry'
19
19
  <%- end -%>
@@ -1,4 +1,7 @@
1
1
  raider-results
2
2
  <%- if rspec? -%>
3
3
  spec/examples.txt
4
+ <%- end -%>
5
+ <%- if cucumber? -%>
6
+ rerun.txt
4
7
  <%- end -%>
@@ -1 +1,6 @@
1
1
  platform: :ios
2
+
3
+ video:
4
+ enabled: true
5
+ fps: 5
6
+ max_width: 1280
@@ -13,4 +13,9 @@ browser_arguments:
13
13
  - search-engine-choice-country
14
14
  firefox:
15
15
  - acceptInsecureCerts
16
- - no-sandbox
16
+ - no-sandbox
17
+
18
+ video:
19
+ enabled: true
20
+ fps: 5
21
+ max_width: 1280
@@ -93,11 +93,13 @@ Every run writes machine-readable results to `raider-results/` (including `run.j
93
93
  `features/support/env.rb` requires `raider_reporter/cucumber`, so no extra Cucumber formatter flag is needed.
94
94
  <%- end -%>
95
95
 
96
- To upload runs to Raider, set these environment variables before running the suite:
96
+ To upload runs to Raider, set these environment variables before running the suite.
97
+ `RAIDER_TOKEN` and `RAIDER_ENDPOINT` come from your Raider Hideout server (its setup page, or `bin/rails hideout:setup` /
98
+ `hideout:token`):
97
99
 
98
100
  ```bash
99
101
  export RAIDER_TOKEN=your-token
100
- export RAIDER_ENDPOINT=https://your-raider-endpoint
102
+ export RAIDER_ENDPOINT=https://hideout.example.com
101
103
  ```
102
104
 
103
105
  <%- if rspec? -%>
@@ -108,6 +110,62 @@ Without `RAIDER_TOKEN`, nothing is uploaded and the suite output is unchanged. F
108
110
  report and to Raider. See the [raider_reporter README](https://github.com/RaiderHQ/raider_reporter) for details.
109
111
  <%- end -%>
110
112
 
113
+ ### Video replay
114
+
115
+ Every test is recorded, and the video is kept only for failing tests<%= rspec? ? ' (on the last retry attempt)' : '' %>. It is attached
116
+ to the report as `video` and saved under `raider-results/attachments/`, next to the failure screenshot.
117
+ <%- if mobile? -%>
118
+ Appium records the device screen and the video is attached as `video.mp4`.
119
+ <%- else -%>
120
+ The video is a `video.webm` built from page screenshots with ffmpeg.
121
+ <%- end -%>
122
+
123
+ <%- if single_platform? -%>
124
+ - Turn video off for one run with `VIDEO=off bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`, or for the project with
125
+ `raider u video off`, which creates `config/config.yml` with a `video:` section (`raider u video on` turns it back
126
+ on).
127
+ <%- else -%>
128
+ - Video is on by default: `video:` in `config/config.yml` (`enabled: true`, `fps: 5`, `max_width: 1280`).
129
+ - Turn it off for one run with `VIDEO=off bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`, or for the project with
130
+ `raider u video off` (`raider u video on` turns it back on).
131
+ <%- end -%>
132
+ <%- unless mobile? -%>
133
+ - ffmpeg is downloaded on first use, checked against a pinned SHA-256 and cached in `~/.cache/raider`
134
+ (`%LOCALAPPDATA%\raider` on Windows). `raider u video install` downloads it ahead of time (CI does this before the
135
+ tests and caches the folder), and `raider u video status` prints the binary in use.
136
+ - `RAIDER_FFMPEG_PATH` uses your own ffmpeg (it needs libvpx) instead of the download, and `RAIDER_CACHE_DIR` moves
137
+ the cache.
138
+ <%- if azure? -%>
139
+ - `azure-pipelines.yml` sets `RAIDER_CACHE_DIR` to `$(Pipeline.Workspace)/.cache/raider`, the folder its `Cache@2`
140
+ step keeps between runs.
141
+ <%- end -%>
142
+ - The RaiderHQ `ffmpeg-7.1` release that the download comes from is not published yet. Until it is, set
143
+ `RAIDER_FFMPEG_PATH` to get videos; without it the suite runs as usual and prints one `[video]` line.
144
+ - If ffmpeg is missing or recording fails, the suite still runs and its exit code is unchanged; you get one `[video]`
145
+ line on stderr and no video.
146
+ - Known limits: a frame is taken after each page load, click and text change, plus one at the end of a failing test,
147
+ so changes the page makes on its own between actions show up in the next frame. There is one recorded driver per
148
+ process.
149
+ <%- else -%>
150
+ - Appium's screen recording needs `ffmpeg` on the Appium server host's `PATH` (this machine only when Appium runs
151
+ here): always for iOS (XCUITest), and for Android when a recording is longer than 180 s. Without it, recording
152
+ fails with one `[video]` line and the tests run as usual.
153
+ <%- if cross_platform? -%>
154
+ - Each test records for at most 1800 s on iOS and 180 s on Android, so Android needs no ffmpeg on the Appium host.
155
+ <%- elsif ios? -%>
156
+ - Each test records for at most 1800 s.
157
+ <%- else -%>
158
+ - Each test records for at most 180 s, so the Appium host needs no ffmpeg; longer tests keep only their first 180 s.
159
+ <%- end -%>
160
+ Set `time_limit` (seconds) under `video:` in `config/config.yml`<%= single_platform? ? ' (create it if it is missing)' : '' %> to change it; on Android, more
161
+ than 180 s needs ffmpeg on the Appium host.
162
+ - `raider u video install` and `raider u video status` manage the ffmpeg that web projects use, cached in
163
+ `~/.cache/raider` (override with `RAIDER_FFMPEG_PATH` or `RAIDER_CACHE_DIR`); they do not install ffmpeg on the
164
+ Appium host. The RaiderHQ `ffmpeg-7.1` release it comes from is not published yet.
165
+ - If recording fails, the suite still runs and its exit code is unchanged; you get one `[video]` line on stderr and
166
+ no video.
167
+ <%- end -%>
168
+
111
169
  <%- unless azure? && mobile? -%>
112
170
  ### Quarantine
113
171
 
@@ -3,6 +3,7 @@
3
3
  require 'active_support/core_ext/string/inflections'
4
4
  require 'yaml'
5
5
  require 'watir'
6
+ require_relative 'video_helper'
6
7
 
7
8
  module BrowserHelper
8
9
 
@@ -23,6 +24,9 @@ module BrowserHelper
23
24
  args += ['--headless'] if (ENV['HEADLESS'] || config['headless']) && !args.include?('--headless')
24
25
  capitalized = browser_name == 'ie' ? browser_name.upcase : browser_name.capitalize
25
26
  browser_options = "Selenium::WebDriver::#{capitalized}::Options".constantize.new(args:)
26
- Watir::Browser.new(browser_name.to_sym, options: browser_options)
27
+ arguments = { options: browser_options }
28
+ listener = VideoHelper.listener
29
+ arguments[:listener] = listener if listener
30
+ Watir::Browser.new(browser_name.to_sym, **arguments)
27
31
  end
28
32
  end
@@ -4,6 +4,7 @@ require 'yaml'
4
4
  <%- if selenium_based? -%>
5
5
  require 'active_support/inflector'
6
6
  require 'selenium-webdriver'
7
+ require_relative 'video_helper'
7
8
  <%- else -%>
8
9
  require 'appium_lib'
9
10
  <%- end -%>
@@ -1,7 +1,10 @@
1
1
  def create_driver(*opts)
2
2
  @config = YAML.load_file('config/config.yml')
3
3
  browser = @config['browser']
4
- driver = Selenium::WebDriver.for(browser.to_sym, options: create_webdriver_options(*opts))
4
+ arguments = { options: create_webdriver_options(*opts) }
5
+ listener = VideoHelper.listener
6
+ arguments[:listener] = listener if listener
7
+ driver = Selenium::WebDriver.for(browser.to_sym, **arguments)
5
8
  driver.manage.timeouts.implicit_wait = @config.fetch('timeout', 10)
6
9
  driver
7
10
  end
@@ -12,6 +12,7 @@ require_relative 'browser_helper'
12
12
  <%- else -%>
13
13
  require_relative 'driver_helper'
14
14
  <%- end -%>
15
+ require_relative 'video_helper'
15
16
 
16
17
  module SpecHelper
17
18
  RSpec.configure do |config|
@@ -45,10 +46,14 @@ module SpecHelper
45
46
  browser.window.maximize
46
47
  end
47
48
  <%- end -%>
49
+ @video_recorder = VideoHelper.recorder_for(<%= watir? ? 'browser' : 'driver' %>)
50
+ @video_recorder.start
48
51
  end
49
52
 
50
53
  config.after(:each) do |example|
51
54
  example_name = example.description
55
+ keep_video = example.exception ? VideoHelper.final_attempt?(example) : false
56
+ video_path = @video_recorder&.stop(keep: keep_video)
52
57
  if example.exception
53
58
  @_exception = example.exception
54
59
  Dir.mktmpdir do |temp_folder|
@@ -57,6 +62,9 @@ module SpecHelper
57
62
  RaiderReporter.attach('screenshot', screenshot_path, type: :png)
58
63
  end
59
64
  end
65
+ RaiderReporter.attach('video', video_path, type: <%= mobile? ? ':mp4' : ':webm' %>) if video_path
66
+ ensure
67
+ @video_recorder&.cleanup
60
68
  <%= partial('quit_driver', strip: true) %>
61
69
  end
62
70
  end