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.
- 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 +12 -1
- data/lib/generators/infrastructure/templates/github.tt +12 -0
- data/lib/generators/infrastructure/templates/github_appium.tt +23 -0
- data/lib/generators/invoke_generators.rb +20 -2
- data/lib/generators/rspec/rspec_generator.rb +0 -1
- data/lib/generators/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 +60 -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 +29 -0
- data/spec/integration/content/ci_content_spec.rb +43 -0
- 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 +51 -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
|
@@ -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.
|
|
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\.
|
|
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.
|
|
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\.
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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 #{
|
|
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.
|
|
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\.
|
|
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.
|
|
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\.
|
|
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
|
|
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(
|
|
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 =
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
expect(out).to include('
|
|
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
|
|
@@ -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
|
|
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)
|
|
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")
|
|
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(
|
|
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
|
-
|
|
330
|
-
|
|
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
|
|
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
|
-
|
|
463
|
-
|
|
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
|