ruby_raider 3.8.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.
- 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 +22 -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/partials/appium_env.tt +6 -0
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +6 -0
- data/lib/generators/cucumber/templates/partials/watir_env.tt +6 -0
- data/lib/generators/helper_generator.rb +5 -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 +26 -2
- data/lib/generators/infrastructure/templates/github.tt +31 -1
- data/lib/generators/infrastructure/templates/github_appium.tt +50 -1
- data/lib/generators/invoke_generators.rb +20 -2
- data/lib/generators/rspec/rspec_generator.rb +0 -1
- data/lib/generators/templates/common/gemfile.tt +1 -1
- data/lib/generators/templates/common/git_ignore.tt +3 -0
- data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
- data/lib/generators/templates/common/partials/web_config.tt +6 -1
- data/lib/generators/templates/common/read_me.tt +80 -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/spec_helper.tt +8 -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 +3 -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 +48 -3
- data/spec/integration/content/ci_content_spec.rb +115 -5
- data/spec/integration/content/common_content_spec.rb +8 -0
- data/spec/integration/content/config_content_spec.rb +14 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +22 -1
- data/spec/integration/content/gemfile_content_spec.rb +2 -3
- data/spec/integration/content/helper_content_spec.rb +146 -0
- data/spec/integration/content/page_content_spec.rb +0 -1
- data/spec/integration/content/readme_content_spec.rb +65 -0
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/test_content_spec.rb +2 -3
- data/spec/integration/end_to_end_features_spec.rb +0 -2
- data/spec/integration/end_to_end_spec.rb +252 -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/invoke_generators_spec.rb +52 -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 +49 -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)',
|
|
@@ -54,16 +82,32 @@ describe 'Azure Pipelines content' do
|
|
|
54
82
|
expect(yml['variables']['HEADLESS']).to eq('true')
|
|
55
83
|
end
|
|
56
84
|
|
|
57
|
-
it 'has a raider_comment job for pull requests' do
|
|
85
|
+
it 'has a raider_comment job for pull requests that waits for the gate job but runs even when it is skipped' do
|
|
58
86
|
comment = yml['jobs'][1]
|
|
59
87
|
expect(comment['job']).to eq('raider_comment')
|
|
60
|
-
expect(comment['dependsOn']).to eq(
|
|
88
|
+
expect(comment['dependsOn']).to eq(%w[tests raider_gate])
|
|
61
89
|
expect(comment['condition']).to eq("and(always(), eq(variables['Build.Reason'], 'PullRequest'))")
|
|
62
90
|
last = comment['steps'].last
|
|
63
91
|
expect(last['script']).to eq('bundle exec raider-reporter comment')
|
|
64
92
|
expect(last['env']).to include('SYSTEM_ACCESSTOKEN' => '$(System.AccessToken)')
|
|
65
93
|
end
|
|
66
94
|
|
|
95
|
+
it 'has a raider_gate job that runs only when RAIDER_GATE is 1' do
|
|
96
|
+
gate = yml['jobs'][2]
|
|
97
|
+
expect(yml['jobs'].size).to eq(3)
|
|
98
|
+
expect(gate['job']).to eq('raider_gate')
|
|
99
|
+
expect(gate['dependsOn']).to eq('tests')
|
|
100
|
+
expect(gate['condition']).to eq("and(always(), eq(variables['RAIDER_GATE'], '1'))")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'sets up Ruby in the raider_gate job and runs the gate with the token env' do
|
|
104
|
+
gate = yml['jobs'][2]
|
|
105
|
+
expect(gate['steps'].first).to eq('task' => 'UseRubyVersion@0', 'inputs' => { 'versionSpec' => '3.4' })
|
|
106
|
+
expect(gate['steps'].filter_map { |s| s['script'] }).to eq(['bundle install', 'bundle exec raider-reporter gate'])
|
|
107
|
+
expect(gate['steps'].last['env']).to eq('RAIDER_TOKEN' => '$(RAIDER_TOKEN)', 'RAIDER_ENDPOINT' => '$(RAIDER_ENDPOINT)',
|
|
108
|
+
'SYSTEM_ACCESSTOKEN' => '$(System.AccessToken)')
|
|
109
|
+
end
|
|
110
|
+
|
|
67
111
|
it 'has triggers, pool and the self-hosted hint' do
|
|
68
112
|
expect(yml['trigger']['branches']['include']).to include('main', 'master')
|
|
69
113
|
expect(yml['pr']['branches']['include']).to include('*')
|
|
@@ -84,8 +128,9 @@ describe 'Azure Pipelines content' do
|
|
|
84
128
|
readme = read_generated(name, 'Readme.md')
|
|
85
129
|
expect(readme).to include('## Azure Pipelines', 'RAIDER_TOKEN', 'Contribute to pull requests',
|
|
86
130
|
'pool: { name: Default }', 'Build validation branch policy',
|
|
87
|
-
'same stage')
|
|
131
|
+
'same stage', 'raider_gate', 'RAIDER_GATE=1', 'RAIDER_SHARD_TOTAL', 'RAIDER_SHARD_INDEX')
|
|
88
132
|
expect(readme).not_to match(/GitHub Actions/i)
|
|
133
|
+
expect(readme).to include('`RAIDER_CACHE_DIR` to `$(Pipeline.Workspace)/.cache/raider`')
|
|
89
134
|
end
|
|
90
135
|
end
|
|
91
136
|
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|
|
|
@@ -92,7 +93,7 @@ describe 'CI pipeline content' do
|
|
|
92
93
|
|
|
93
94
|
it 'uploads raider-results as an artifact even on failure in every job' do
|
|
94
95
|
uploads = steps.select { |step| step['uses'] == 'actions/upload-artifact@v4' }
|
|
95
|
-
expect(uploads.size).to eq(YAML.safe_load(pipeline)['jobs'].except('raider-comment').size)
|
|
96
|
+
expect(uploads.size).to eq(YAML.safe_load(pipeline)['jobs'].except('raider-comment', 'raider-gate').size)
|
|
96
97
|
uploads.each do |upload|
|
|
97
98
|
expect(upload['with']['path']).to eq('raider-results/')
|
|
98
99
|
expect(upload['if']).to eq('always()')
|
|
@@ -107,15 +108,16 @@ describe 'CI pipeline content' do
|
|
|
107
108
|
let(:job) { jobs['raider-comment'] }
|
|
108
109
|
|
|
109
110
|
it 'is a valid workflow with exactly the expected jobs' do
|
|
110
|
-
expect(jobs.keys).to match_array(test_jobs + [
|
|
111
|
+
expect(jobs.keys).to match_array(test_jobs + %w[raider-comment raider-gate])
|
|
111
112
|
end
|
|
112
113
|
|
|
113
|
-
it 'needs every test job' do
|
|
114
|
-
expect(job['needs']).to match_array(test_jobs)
|
|
114
|
+
it 'needs every test job and the gate job, so the comment carries the final gate line' do
|
|
115
|
+
expect(job['needs']).to match_array(test_jobs + %w[raider-gate])
|
|
115
116
|
end
|
|
116
117
|
|
|
117
|
-
it 'runs always, on pull requests only' do
|
|
118
|
+
it 'runs always, on pull requests only, so a skipped or failed gate job does not skip it' do
|
|
118
119
|
expect(job['if']).to eq("always() && github.event_name == 'pull_request'")
|
|
120
|
+
expect(job['if']).to start_with('always()')
|
|
119
121
|
expect(job['runs-on']).to eq('ubuntu-latest')
|
|
120
122
|
end
|
|
121
123
|
|
|
@@ -151,62 +153,170 @@ describe 'CI pipeline content' do
|
|
|
151
153
|
end
|
|
152
154
|
end
|
|
153
155
|
|
|
156
|
+
shared_examples 'github raider-gate job' do |project_name, file, test_jobs|
|
|
157
|
+
subject(:pipeline) { read_generated(project_name, ".github/workflows/#{file}") }
|
|
158
|
+
|
|
159
|
+
let(:jobs) { YAML.safe_load(pipeline)['jobs'] }
|
|
160
|
+
let(:gate_job) { jobs['raider-gate'] }
|
|
161
|
+
|
|
162
|
+
it 'needs every test job and runs always when RAIDER_GATE is 1, on push and pull requests' do
|
|
163
|
+
expect(gate_job['needs']).to match_array(test_jobs)
|
|
164
|
+
expect(gate_job['if']).to eq("always() && vars.RAIDER_GATE == '1'")
|
|
165
|
+
expect(gate_job['runs-on']).to eq('ubuntu-latest')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'can read the code and write statuses only' do
|
|
169
|
+
expect(gate_job['permissions']).to eq('contents' => 'read', 'statuses' => 'write')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'checks out, sets up the same Ruby as the test jobs and runs the gate' do
|
|
173
|
+
steps = gate_job['steps']
|
|
174
|
+
expect(steps[0]).to eq('uses' => 'actions/checkout@v4')
|
|
175
|
+
test_ruby = jobs.values_at(*test_jobs).flat_map { |j| j['steps'] }
|
|
176
|
+
.find { |step| step['uses'] == 'ruby/setup-ruby@v1' }['with']['ruby-version']
|
|
177
|
+
expect(steps[1]).to eq('uses' => 'ruby/setup-ruby@v1', 'with' => { 'ruby-version' => test_ruby, 'bundler-cache' => true })
|
|
178
|
+
expect(steps[2]).to eq('name' => 'Wait for the Raider quality gate',
|
|
179
|
+
'run' => 'bundle exec raider-reporter gate',
|
|
180
|
+
'env' => { 'RAIDER_TOKEN' => '${{ secrets.RAIDER_TOKEN }}',
|
|
181
|
+
'RAIDER_ENDPOINT' => '${{ vars.RAIDER_ENDPOINT }}',
|
|
182
|
+
'GITHUB_TOKEN' => '${{ secrets.GITHUB_TOKEN }}' })
|
|
183
|
+
expect(steps.size).to eq(3)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
shared_examples 'github appium shards' do |project_name, runner, shards|
|
|
188
|
+
subject(:pipeline) { read_generated(project_name, '.github/workflows/appium_pipeline.yml') }
|
|
189
|
+
|
|
190
|
+
let(:jobs) { YAML.safe_load(pipeline)['jobs'] }
|
|
191
|
+
|
|
192
|
+
def test_step(job, runner)
|
|
193
|
+
job['steps'].find { |step| step['run'].to_s.include?(runner) || step['with'].to_h['script'].to_s.include?(runner) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if shards
|
|
197
|
+
it 'gives each platform job its own shard of two, so the gate waits for both' do
|
|
198
|
+
shards.each do |job_name, index|
|
|
199
|
+
expect(test_step(jobs[job_name], runner)['env'])
|
|
200
|
+
.to include('RAIDER_SHARD_INDEX' => index, 'RAIDER_SHARD_TOTAL' => '2')
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
it 'sets no shard env for a single platform' do
|
|
205
|
+
jobs.except('raider-comment', 'raider-gate').each_value do |job|
|
|
206
|
+
expect(test_step(job, runner)['env'].keys).not_to include('RAIDER_SHARD_INDEX', 'RAIDER_SHARD_TOTAL')
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
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
|
+
|
|
154
248
|
context 'with rspec and selenium' do
|
|
155
249
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
250
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build]
|
|
156
251
|
include_examples 'valid github pipeline', name
|
|
157
252
|
include_examples 'github rspec pipeline', name
|
|
158
253
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
159
254
|
include_examples 'github raider wiring', name, 'test_pipeline.yml'
|
|
160
255
|
include_examples 'github raider-comment job', name, 'test_pipeline.yml', %w[build]
|
|
256
|
+
include_examples 'github raider-gate job', name, 'test_pipeline.yml', %w[build]
|
|
161
257
|
include_examples 'github without allure steps', name
|
|
162
258
|
end
|
|
163
259
|
|
|
164
260
|
context 'with cucumber and selenium' do
|
|
165
261
|
name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
|
|
262
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build], 'cucumber'
|
|
166
263
|
include_examples 'valid github pipeline', name
|
|
167
264
|
include_examples 'github cucumber pipeline', name
|
|
168
265
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
169
266
|
include_examples 'github raider wiring', name, 'test_pipeline.yml', 'cucumber'
|
|
170
267
|
include_examples 'github raider-comment job', name, 'test_pipeline.yml', %w[build]
|
|
268
|
+
include_examples 'github raider-gate job', name, 'test_pipeline.yml', %w[build]
|
|
171
269
|
include_examples 'github without allure steps', name
|
|
172
270
|
end
|
|
173
271
|
|
|
174
272
|
context 'with rspec and watir' do
|
|
175
273
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
274
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build]
|
|
176
275
|
include_examples 'valid github pipeline', name
|
|
177
276
|
include_examples 'github rspec pipeline', name
|
|
178
277
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
179
278
|
include_examples 'github raider wiring', name, 'test_pipeline.yml'
|
|
180
279
|
include_examples 'github raider-comment job', name, 'test_pipeline.yml', %w[build]
|
|
280
|
+
include_examples 'github raider-gate job', name, 'test_pipeline.yml', %w[build]
|
|
181
281
|
include_examples 'github without allure steps', name
|
|
182
282
|
end
|
|
183
283
|
|
|
184
284
|
context 'with cucumber and watir' do
|
|
185
285
|
name = "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}"
|
|
286
|
+
include_examples 'github ffmpeg cache', name, 'test_pipeline.yml', %w[build], 'cucumber'
|
|
186
287
|
include_examples 'valid github pipeline', name
|
|
187
288
|
include_examples 'github cucumber pipeline', name
|
|
188
289
|
include_examples 'github triggers', name, 'test_pipeline.yml'
|
|
189
290
|
include_examples 'github raider wiring', name, 'test_pipeline.yml', 'cucumber'
|
|
190
291
|
include_examples 'github raider-comment job', name, 'test_pipeline.yml', %w[build]
|
|
292
|
+
include_examples 'github raider-gate job', name, 'test_pipeline.yml', %w[build]
|
|
191
293
|
include_examples 'github without allure steps', name
|
|
192
294
|
end
|
|
193
295
|
|
|
194
296
|
appium_jobs = { 'android' => %w[android-tests], 'ios' => %w[ios-tests],
|
|
195
297
|
'cross_platform' => %w[ios-tests android-tests] }
|
|
196
298
|
|
|
299
|
+
appium_shards = { 'cross_platform' => { 'ios-tests' => '1', 'android-tests' => '2' } }
|
|
300
|
+
|
|
197
301
|
appium_jobs.each do |automation, test_jobs|
|
|
198
302
|
context "with rspec and #{automation}" do
|
|
199
303
|
name = "#{FrameworkIndex::RSPEC}_#{automation}"
|
|
304
|
+
include_examples 'github appium shards', name, 'rspec', appium_shards[automation]
|
|
305
|
+
include_examples 'github ffmpeg cache', name, 'appium_pipeline.yml', test_jobs
|
|
200
306
|
include_examples 'github raider-comment job', name, 'appium_pipeline.yml', test_jobs
|
|
307
|
+
include_examples 'github raider-gate job', name, 'appium_pipeline.yml', test_jobs
|
|
201
308
|
include_examples 'github triggers', name, 'appium_pipeline.yml'
|
|
202
309
|
include_examples 'github raider wiring', name, 'appium_pipeline.yml'
|
|
203
310
|
end
|
|
204
311
|
|
|
205
312
|
context "with cucumber and #{automation}" do
|
|
206
313
|
name = "#{FrameworkIndex::CUCUMBER}_#{automation}"
|
|
314
|
+
include_examples 'github appium shards', name, 'cucumber', appium_shards[automation]
|
|
315
|
+
include_examples 'github ffmpeg cache', name, 'appium_pipeline.yml', test_jobs, 'cucumber'
|
|
207
316
|
include_examples 'github triggers', name, 'appium_pipeline.yml'
|
|
208
317
|
include_examples 'github raider wiring', name, 'appium_pipeline.yml', 'cucumber'
|
|
209
318
|
include_examples 'github raider-comment job', name, 'appium_pipeline.yml', test_jobs
|
|
319
|
+
include_examples 'github raider-gate job', name, 'appium_pipeline.yml', test_jobs
|
|
210
320
|
end
|
|
211
321
|
end
|
|
212
322
|
end
|
|
@@ -133,6 +133,10 @@ 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
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
# --- Spec helper retry config (C4) ---
|
|
@@ -187,6 +191,10 @@ describe 'Common file content' do
|
|
|
187
191
|
it 'includes --retry flag' do
|
|
188
192
|
expect(cucumber_yml).to include('--retry')
|
|
189
193
|
end
|
|
194
|
+
|
|
195
|
+
it 'writes failed scenarios to rerun.txt for Re-run Failed' do
|
|
196
|
+
expect(cucumber_yml).to include('--format rerun --out rerun.txt')
|
|
197
|
+
end
|
|
190
198
|
end
|
|
191
199
|
|
|
192
200
|
# --- RSpec contexts ---
|
|
@@ -51,12 +51,21 @@ 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
|
+
|
|
54
62
|
# --- Web config per automation ---
|
|
55
63
|
|
|
56
64
|
context 'with rspec and selenium' do
|
|
57
65
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
58
66
|
include_examples 'valid web config', name
|
|
59
67
|
include_examples 'selenium config with timeout', name
|
|
68
|
+
include_examples 'video config', name
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
context 'with rspec and watir' do
|
|
@@ -73,6 +82,11 @@ describe 'Config file content' do
|
|
|
73
82
|
|
|
74
83
|
# --- Mobile config ---
|
|
75
84
|
|
|
85
|
+
# Single-platform Appium projects have no config.yml; VideoHelper falls back to its defaults.
|
|
86
|
+
context 'with rspec and cross platform' do
|
|
87
|
+
include_examples 'video config', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
|
|
88
|
+
end
|
|
89
|
+
|
|
76
90
|
context 'with rspec and appium android' do
|
|
77
91
|
subject(:caps) { read_generated("#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}", 'config/capabilities.yml') }
|
|
78
92
|
|
|
@@ -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
|
|
@@ -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
|