ruby_raider 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +23 -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/feature.tt +5 -0
- data/lib/generators/cucumber/templates/partials/appium_env.tt +6 -0
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -0
- data/lib/generators/cucumber/templates/partials/watir_env.tt +22 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
- data/lib/generators/helper_generator.rb +12 -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/rspec/templates/spec.tt +12 -0
- data/lib/generators/templates/common/gemfile.tt +1 -1
- data/lib/generators/templates/common/git_ignore.tt +7 -1
- data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
- data/lib/generators/templates/common/partials/web_config.tt +9 -1
- data/lib/generators/templates/common/read_me.tt +84 -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/session_state.tt +206 -0
- data/lib/generators/templates/helpers/spec_helper.tt +28 -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 +4 -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 +33 -0
- data/spec/integration/content/ci_content_spec.rb +54 -0
- data/spec/integration/content/common_content_spec.rb +35 -0
- data/spec/integration/content/config_content_spec.rb +32 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +47 -1
- data/spec/integration/content/gemfile_content_spec.rb +2 -3
- data/spec/integration/content/helper_content_spec.rb +178 -0
- data/spec/integration/content/page_content_spec.rb +0 -1
- data/spec/integration/content/readme_content_spec.rb +79 -0
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/syntax_validation_spec.rb +15 -0
- data/spec/integration/content/test_content_spec.rb +27 -3
- data/spec/integration/end_to_end_features_spec.rb +0 -2
- data/spec/integration/end_to_end_spec.rb +275 -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/helpers_generator_spec.rb +22 -0
- data/spec/integration/generators/invoke_generators_spec.rb +52 -0
- data/spec/integration/helpers/session_state_spec.rb +428 -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 +65 -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,57 @@
|
|
|
3
3
|
require_relative 'content_helper'
|
|
4
4
|
|
|
5
5
|
describe 'Generated Readme content' do
|
|
6
|
+
FRAMEWORKS.product(AUTOMATION_TYPES).each do |framework, automation|
|
|
7
|
+
context "with #{framework} and #{automation}, the Video replay section" do
|
|
8
|
+
subject(:section) { readme[/^### Video replay\n.*?(?=^##)/m]&.gsub(/\s+/, ' ') }
|
|
9
|
+
|
|
10
|
+
let(:readme) { read_generated("#{framework}_#{automation}", 'Readme.md') }
|
|
11
|
+
let(:web) { %w[selenium watir].include?(automation) }
|
|
12
|
+
|
|
13
|
+
it 'exists and covers the defaults, VIDEO=off, the install commands and the cache' do
|
|
14
|
+
expect(section).not_to be_nil
|
|
15
|
+
expect(section).to include('only for failing tests', 'VIDEO=off', 'raider u video install',
|
|
16
|
+
'raider u video status', '~/.cache/raider', 'RAIDER_FFMPEG_PATH',
|
|
17
|
+
'RAIDER_CACHE_DIR', 'raider-results/attachments/')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'says that video needs RAIDER_FFMPEG_PATH until the ffmpeg release is published' do
|
|
21
|
+
expect(section).to include('ffmpeg-7.1', 'not published yet')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'names the right video format and how to turn video off for this project' do
|
|
25
|
+
if web
|
|
26
|
+
expect(section).to include('video.webm', 'raider u video off', 'raider u video on', 'video:')
|
|
27
|
+
elsif %w[android ios].include?(automation)
|
|
28
|
+
expect(section).to include('video.mp4', 'raider u video off', 'creates `config/config.yml`')
|
|
29
|
+
expect(section).not_to include('no `config/config.yml`')
|
|
30
|
+
else
|
|
31
|
+
expect(section).to include('video.mp4', 'raider u video off')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'says where mobile recording needs ffmpeg and how long it records' do
|
|
36
|
+
limits = { 'ios' => ['1800 s'], 'android' => ['180 s'], 'cross_platform' => ['1800 s', '180 s'] }
|
|
37
|
+
if web
|
|
38
|
+
expect(section).not_to include('Appium server host')
|
|
39
|
+
else
|
|
40
|
+
expect(section).to include('`ffmpeg` on the Appium server host', 'iOS', 'longer than 180 s',
|
|
41
|
+
'time_limit', *limits.fetch(automation))
|
|
42
|
+
expect(section).not_to include('do not use ffmpeg')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'lists the known limits of web recording' do
|
|
47
|
+
if web
|
|
48
|
+
expect(section).to include('after each page load, click and text change', 'next frame',
|
|
49
|
+
'one recorded driver per process')
|
|
50
|
+
else
|
|
51
|
+
expect(section).not_to include('next frame')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
6
57
|
AUTOMATION_TYPES.each do |automation|
|
|
7
58
|
context "with rspec and #{automation}" do
|
|
8
59
|
subject(:readme) { read_generated("#{FrameworkIndex::RSPEC}_#{automation}", 'Readme.md') }
|
|
@@ -61,4 +112,32 @@ describe 'Generated Readme content' do
|
|
|
61
112
|
end
|
|
62
113
|
end
|
|
63
114
|
end
|
|
115
|
+
|
|
116
|
+
FRAMEWORKS.product(AUTOMATION_TYPES).each do |framework, automation|
|
|
117
|
+
context "with #{framework} and #{automation}, the login reuse section" do
|
|
118
|
+
subject(:readme) { read_generated("#{framework}_#{automation}", 'Readme.md') }
|
|
119
|
+
|
|
120
|
+
if %w[selenium watir].include?(automation)
|
|
121
|
+
it 'documents the tags, other roles, max_age, the env switch and the secrets warning' do
|
|
122
|
+
section = readme[/^## Reusing the login\n.*?(?=^##)/m].gsub(/\s+/, ' ')
|
|
123
|
+
expect(section).to include('`:logged_in`', '`@logged_in`', "SessionState.new(driver, name: 'admin')",
|
|
124
|
+
'browser.driver', 'login_state', 'max_age', 'RAIDER_LOGIN_STATE=off',
|
|
125
|
+
'RAIDER_LOGIN_STATE=refresh', '.raider/auth/', 'git-ignored',
|
|
126
|
+
'live session tokens', 'must not be committed, cached or uploaded')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'is its own ## section, straight after the Quality gate and before any CI section' do
|
|
130
|
+
heading = readme.index("\n## Reusing the login\n")
|
|
131
|
+
gate = readme.index("\n### Quality gate\n")
|
|
132
|
+
expect(gate).to be < heading
|
|
133
|
+
expect(readme[(gate + 1)...heading]).not_to match(/\n##/)
|
|
134
|
+
expect(readme[(heading + 1)..][/\n## .*/]).to be_nil.or match(/\A\n## (CI|Azure Pipelines)\z/)
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
it 'does not mention login reuse' do
|
|
138
|
+
expect(readme).not_to include('Reusing the login', 'RAIDER_LOGIN_STATE', '.raider/auth/')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
64
143
|
end
|
|
@@ -18,8 +18,8 @@ describe 'RSpec projects without Allure' do
|
|
|
18
18
|
expect(File).not_to exist(File.join(project_name, 'allure-results'))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
it 'pins raider_reporter 0.
|
|
22
|
-
expect(read_generated(project_name, 'Gemfile')).to include("gem 'raider_reporter', '~> 0.
|
|
21
|
+
it 'pins raider_reporter 0.10' do
|
|
22
|
+
expect(read_generated(project_name, 'Gemfile')).to include("gem 'raider_reporter', '~> 0.10'")
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -24,6 +24,21 @@ describe 'Ruby syntax validation' do
|
|
|
24
24
|
expect(content).to have_frozen_string_literal
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
login_reuse_files = %w[helpers/session_state.rb]
|
|
29
|
+
login_reuse_files += if framework == 'rspec'
|
|
30
|
+
%w[helpers/spec_helper.rb]
|
|
31
|
+
else
|
|
32
|
+
%w[features/support/env.rb features/step_definitions/login_steps.rb]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
login_reuse_files.each do |relative|
|
|
36
|
+
it "#{relative} is generated as valid Ruby with frozen_string_literal" do
|
|
37
|
+
content = File.read(File.join(project_name, relative))
|
|
38
|
+
expect(content).to have_valid_ruby_syntax
|
|
39
|
+
expect(content).to have_frozen_string_literal
|
|
40
|
+
end
|
|
41
|
+
end
|
|
27
42
|
end
|
|
28
43
|
end
|
|
29
44
|
end
|
|
@@ -111,8 +111,29 @@ describe 'Test file content' do
|
|
|
111
111
|
expect(config).to include('default:')
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
it 'uses the pretty
|
|
115
|
-
expect(config.strip).to eq('default: --format pretty --publish-quiet --retry 0')
|
|
114
|
+
it 'uses the pretty and rerun formatters, with the reporter activated from env.rb' do
|
|
115
|
+
expect(config.strip).to eq('default: --format pretty --format rerun --out rerun.txt --publish-quiet --retry 0')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
shared_examples 'logged in example' do |project_name|
|
|
120
|
+
it 'adds a :logged_in example that reuses the saved login and keeps the login example' do
|
|
121
|
+
spec = read_generated(project_name, 'spec/login_page_spec.rb')
|
|
122
|
+
expect(spec).to include("describe 'Account', :logged_in", 'opens the account page with the saved login',
|
|
123
|
+
"describe 'Login' do", 'can successfully log in')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
shared_examples 'logged in scenario' do |project_name|
|
|
128
|
+
it 'adds a @logged_in scenario and keeps the login scenario' do
|
|
129
|
+
feature = read_generated(project_name, 'features/login.feature')
|
|
130
|
+
expect(feature).to include("@logged_in\nScenario: A logged-in user opens the account page",
|
|
131
|
+
"Given I'm a registered user\n", 'Scenario: A user can login')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'defines the step that picks the user without logging in' do
|
|
135
|
+
steps = read_generated(project_name, 'features/step_definitions/login_steps.rb')
|
|
136
|
+
expect(steps).to include(%q(Given("I'm a {user}") do |user|))
|
|
116
137
|
end
|
|
117
138
|
end
|
|
118
139
|
|
|
@@ -122,12 +143,14 @@ describe 'Test file content' do
|
|
|
122
143
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
|
|
123
144
|
include_examples 'valid rspec spec', name
|
|
124
145
|
include_examples 'selenium rspec spec', name
|
|
146
|
+
include_examples 'logged in example', name
|
|
125
147
|
end
|
|
126
148
|
|
|
127
149
|
context 'with rspec and watir' do
|
|
128
150
|
name = "#{FrameworkIndex::RSPEC}_#{AutomationIndex::WATIR}"
|
|
129
151
|
include_examples 'valid rspec spec', name
|
|
130
152
|
include_examples 'watir rspec spec', name
|
|
153
|
+
include_examples 'logged in example', name
|
|
131
154
|
end
|
|
132
155
|
|
|
133
156
|
# --- Cucumber contexts ---
|
|
@@ -138,6 +161,7 @@ describe 'Test file content' do
|
|
|
138
161
|
include_examples 'valid cucumber env', name
|
|
139
162
|
include_examples 'valid cucumber world', name
|
|
140
163
|
include_examples 'valid cucumber config', name
|
|
164
|
+
include_examples 'logged in scenario', name
|
|
141
165
|
end
|
|
142
166
|
|
|
143
167
|
context 'with cucumber and watir' do
|
|
@@ -146,6 +170,6 @@ describe 'Test file content' do
|
|
|
146
170
|
include_examples 'valid cucumber env', name
|
|
147
171
|
include_examples 'valid cucumber world', name
|
|
148
172
|
include_examples 'valid cucumber config', name
|
|
173
|
+
include_examples 'logged in scenario', name
|
|
149
174
|
end
|
|
150
|
-
|
|
151
175
|
end
|
|
@@ -242,7 +242,6 @@ describe 'End-to-End Feature Validation' do
|
|
|
242
242
|
end
|
|
243
243
|
end
|
|
244
244
|
end
|
|
245
|
-
|
|
246
245
|
end
|
|
247
246
|
|
|
248
247
|
# --- Feature: GitHub Actions CI ---
|
|
@@ -264,5 +263,4 @@ describe 'End-to-End Feature Validation' do
|
|
|
264
263
|
expect(content).to include('rspec')
|
|
265
264
|
end
|
|
266
265
|
end
|
|
267
|
-
|
|
268
266
|
end
|
|
@@ -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)
|
|
@@ -247,6 +269,29 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
247
269
|
context 'Web Automation Frameworks' do
|
|
248
270
|
describe 'Selenium + RSpec' do
|
|
249
271
|
include_examples 'executable rspec project', 'rspec_selenium'
|
|
272
|
+
|
|
273
|
+
it 'saves the login once and reuses it', :aggregate_failures, :slow do
|
|
274
|
+
skip 'Bundle install failed' unless bundle_install('rspec_selenium')
|
|
275
|
+
|
|
276
|
+
state_file = File.join('rspec_selenium', '.raider', 'auth', 'user.json')
|
|
277
|
+
FileUtils.rm_rf(File.join('rspec_selenium', '.raider'))
|
|
278
|
+
first = run_in_project('rspec_selenium', 'bundle exec rspec spec/', env: { 'HEADLESS' => 'true' })
|
|
279
|
+
failed = ->(run, index) { "Run #{index} failed.\nSTDOUT: #{run[:stdout]}\nSTDERR: #{run[:stderr]}" }
|
|
280
|
+
raise failed.call(first, 1) unless first[:exit_code].zero? # show run 1's output, not a missing state file
|
|
281
|
+
|
|
282
|
+
expect(File).to exist(state_file)
|
|
283
|
+
saved = JSON.parse(File.read(state_file))
|
|
284
|
+
second = run_in_project('rspec_selenium', 'bundle exec rspec spec/', env: { 'HEADLESS' => 'true' })
|
|
285
|
+
expect(second[:exit_code]).to eq(0), failed.call(second, 2)
|
|
286
|
+
expect(File.stat(state_file).mode & 0o777).to eq(0o600)
|
|
287
|
+
expect(saved).to include('format' => 1, 'origin' => 'https://raider-test-site.onrender.com')
|
|
288
|
+
expect(JSON.parse(File.read(state_file))['saved_at']).to eq(saved['saved_at'])
|
|
289
|
+
cookie_values = saved['cookies'].map { |cookie| cookie['value'].to_s }
|
|
290
|
+
expect(cookie_values.reject(&:empty?)).not_to be_empty
|
|
291
|
+
output = [first, second].map { |run| run[:stdout] + run[:stderr] }.join
|
|
292
|
+
# Short values like "1" or "en" can appear in any output; a leaked token is long.
|
|
293
|
+
cookie_values.select { |value| value.length >= 8 }.each { |value| expect(output).not_to include(value) }
|
|
294
|
+
end
|
|
250
295
|
end
|
|
251
296
|
|
|
252
297
|
describe 'Watir + RSpec' do
|
|
@@ -263,20 +308,22 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
263
308
|
end
|
|
264
309
|
|
|
265
310
|
# 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
|
-
|
|
311
|
+
def azure_push_env
|
|
312
|
+
{
|
|
313
|
+
'TF_BUILD' => 'True',
|
|
314
|
+
'SYSTEM_COLLECTIONURI' => 'https://dev.azure.com/raider-test/',
|
|
315
|
+
'SYSTEM_TEAMPROJECT' => 'My Project',
|
|
316
|
+
'SYSTEM_TEAMPROJECTID' => '6ce954b1-ce1f-45d1-b94d-e6bf2464ba2c',
|
|
317
|
+
'BUILD_REPOSITORY_NAME' => 'web tests',
|
|
318
|
+
'BUILD_REPOSITORY_ID' => '1c2bd1a0-3f63-4d93-9c3c-2b4f2b1d8f11',
|
|
319
|
+
'BUILD_REPOSITORY_PROVIDER' => 'TfsGit',
|
|
320
|
+
'BUILD_BUILDID' => '42',
|
|
321
|
+
'SYSTEM_JOBDISPLAYNAME' => 'tests',
|
|
322
|
+
'BUILD_REASON' => 'IndividualCI',
|
|
323
|
+
'BUILD_SOURCEBRANCH' => 'refs/heads/release/1.2',
|
|
324
|
+
'BUILD_SOURCEVERSION' => 'abc'
|
|
325
|
+
}.freeze
|
|
326
|
+
end
|
|
280
327
|
|
|
281
328
|
# Generates a ci: azure web project (the shared before(:all) only builds the default github ones)
|
|
282
329
|
def generate_azure_project(framework)
|
|
@@ -289,7 +336,7 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
289
336
|
end
|
|
290
337
|
|
|
291
338
|
def azure_env_prefix
|
|
292
|
-
"GITHUB_ACTIONS= HEADLESS=true #{
|
|
339
|
+
"GITHUB_ACTIONS= HEADLESS=true #{azure_push_env.map { |k, v| "#{k}='#{v}'" }.join(' ')}"
|
|
293
340
|
end
|
|
294
341
|
|
|
295
342
|
def expect_azure_ci_block(project)
|
|
@@ -310,12 +357,12 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
310
357
|
describe 'Selenium + RSpec' do
|
|
311
358
|
let(:project) { generate_azure_project('rspec') }
|
|
312
359
|
|
|
313
|
-
it 'ships azure-pipelines.yml and resolves raider_reporter 0.
|
|
360
|
+
it 'ships azure-pipelines.yml and resolves raider_reporter 0.10+', :aggregate_failures do
|
|
314
361
|
expect(File).to exist(File.join(project, 'azure-pipelines.yml'))
|
|
315
362
|
expect(File).not_to exist(File.join(project, '.github/workflows'))
|
|
316
363
|
skip 'Bundle install failed' unless bundle_install(project)
|
|
317
364
|
|
|
318
|
-
expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.
|
|
365
|
+
expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.1\d\.\d+\)$/)
|
|
319
366
|
end
|
|
320
367
|
|
|
321
368
|
it 'records provider azure_pipelines and an azure_pipelines: build key in run.json', :aggregate_failures, :slow do
|
|
@@ -332,11 +379,11 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
332
379
|
describe 'Selenium + Cucumber' do
|
|
333
380
|
let(:project) { generate_azure_project('cucumber') }
|
|
334
381
|
|
|
335
|
-
it 'ships azure-pipelines.yml and resolves raider_reporter 0.
|
|
382
|
+
it 'ships azure-pipelines.yml and resolves raider_reporter 0.10+', :aggregate_failures do
|
|
336
383
|
expect(File).to exist(File.join(project, 'azure-pipelines.yml'))
|
|
337
384
|
skip 'Bundle install failed' unless bundle_install(project)
|
|
338
385
|
|
|
339
|
-
expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.
|
|
386
|
+
expect(File.read(File.join(project, 'Gemfile.lock'))).to match(/^ raider_reporter \(0\.1\d\.\d+\)$/)
|
|
340
387
|
end
|
|
341
388
|
|
|
342
389
|
it 'records provider azure_pipelines and an azure_pipelines: build key in run.json', :aggregate_failures, :slow do
|
|
@@ -351,6 +398,208 @@ describe 'End-to-End Project Generation and Execution' do
|
|
|
351
398
|
end
|
|
352
399
|
end
|
|
353
400
|
|
|
401
|
+
# Failure video replay against real browsers. Needs an ffmpeg with libvpx (RAIDER_FFMPEG_PATH), because the
|
|
402
|
+
# RaiderHQ ffmpeg release has no checksums yet. Skipped when it, or the browser, is missing. Firefox can be
|
|
403
|
+
# pointed at a specific binary with FIREFOX_BINARY.
|
|
404
|
+
context 'Failure video replay', :slow, :video do
|
|
405
|
+
let(:ffmpeg_path) { ENV.fetch('RAIDER_FFMPEG_PATH', '') }
|
|
406
|
+
let(:cache_dir) { Dir.mktmpdir('raider-cache') }
|
|
407
|
+
|
|
408
|
+
after { FileUtils.rm_rf(cache_dir) }
|
|
409
|
+
|
|
410
|
+
def skip_unless_video_prerequisites(browser)
|
|
411
|
+
skip 'RAIDER_FFMPEG_PATH is not set to an executable ffmpeg with libvpx' unless
|
|
412
|
+
File.file?(ffmpeg_path) && File.executable?(ffmpeg_path)
|
|
413
|
+
skip "#{browser} is not installed" unless browser_installed?(browser)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def browser_installed?(browser)
|
|
417
|
+
if browser == 'firefox'
|
|
418
|
+
ENV['FIREFOX_BINARY'] ? File.executable?(ENV['FIREFOX_BINARY']) : app_or_command?('Firefox', 'firefox')
|
|
419
|
+
else
|
|
420
|
+
app_or_command?('Google Chrome', 'google-chrome') || app_or_command?('Chromium', 'chromium')
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def app_or_command?(app, command)
|
|
425
|
+
File.directory?("/Applications/#{app}.app") || system("command -v #{command} > /dev/null 2>&1")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# A separate project, so the default projects keep resolving raider_reporter 0.10+ from RubyGems
|
|
429
|
+
def generate_video_project(framework, automation)
|
|
430
|
+
name = "video_#{framework}_#{automation}"
|
|
431
|
+
unless LazyProjectGenerator::GENERATED.include?(name)
|
|
432
|
+
generate_framework(framework:, automation:, name:)
|
|
433
|
+
LazyProjectGenerator::GENERATED.add(name)
|
|
434
|
+
use_local_ruby_raider(name)
|
|
435
|
+
expect(run_in_project(name, 'bundle install --quiet', timeout: 300)[:success]).to be(true)
|
|
436
|
+
end
|
|
437
|
+
FileUtils.rm_rf(File.join(name, 'raider-results'))
|
|
438
|
+
name
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def video_env(overrides = {})
|
|
442
|
+
{ 'HEADLESS' => 'true', 'RAIDER_FFMPEG_PATH' => ffmpeg_path, 'RAIDER_CACHE_DIR' => cache_dir,
|
|
443
|
+
'RAIDER_TOKEN' => nil, 'RAIDER_ENDPOINT' => nil, 'VIDEO' => nil }.merge(overrides)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def page_url
|
|
447
|
+
"data:text/html,#{ERB::Util.url_encode('<h1 id="title">Video</h1><input id="name"><button id="go">Go</button>')}"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def add_rspec_video_spec(project)
|
|
451
|
+
File.write(File.join(project, 'spec/video_replay_spec.rb'), <<~SPEC)
|
|
452
|
+
# frozen_string_literal: true
|
|
453
|
+
|
|
454
|
+
require_relative '../helpers/spec_helper'
|
|
455
|
+
|
|
456
|
+
describe 'Video replay' do
|
|
457
|
+
it 'passes after driving the page' do
|
|
458
|
+
driver.navigate.to('#{page_url}')
|
|
459
|
+
driver.find_element(id: 'name').send_keys('ok')
|
|
460
|
+
expect(driver.find_element(id: 'title').text).to eq('Video')
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it 'fails after driving the page' do
|
|
464
|
+
driver.navigate.to('#{page_url}')
|
|
465
|
+
driver.find_element(id: 'name').send_keys('broken')
|
|
466
|
+
driver.find_element(id: 'go').click
|
|
467
|
+
expect(driver.find_element(id: 'title').text).to eq('deliberate video failure')
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
SPEC
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def add_cucumber_video_feature(project, firefox_binary)
|
|
474
|
+
File.write(File.join(project, 'features/video_replay.feature'), <<~FEATURE)
|
|
475
|
+
Feature: Video replay
|
|
476
|
+
|
|
477
|
+
Scenario: A passing scenario
|
|
478
|
+
Given the video page is open
|
|
479
|
+
Then the video page title is "Video"
|
|
480
|
+
|
|
481
|
+
Scenario: A failing scenario
|
|
482
|
+
Given the video page is open
|
|
483
|
+
Then the video page title is "deliberate video failure"
|
|
484
|
+
FEATURE
|
|
485
|
+
File.write(File.join(project, 'features/step_definitions/video_replay_steps.rb'), <<~STEPS)
|
|
486
|
+
# frozen_string_literal: true
|
|
487
|
+
|
|
488
|
+
Given('the video page is open') do
|
|
489
|
+
browser.goto('#{page_url}')
|
|
490
|
+
browser.text_field(id: 'name').set('typed')
|
|
491
|
+
browser.button(id: 'go').click
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
Then('the video page title is {string}') do |title|
|
|
495
|
+
raise "expected title \#{title}" unless browser.h1(id: 'title').text == title
|
|
496
|
+
end
|
|
497
|
+
STEPS
|
|
498
|
+
config = File.join(project, 'config/config.yml')
|
|
499
|
+
File.write(config, File.read(config).sub(/^browser: .*$/, 'browser: firefox'))
|
|
500
|
+
# Test setup only: the generated templates do not read FIREFOX_BINARY.
|
|
501
|
+
File.write(File.join(project, 'features/support/zz_firefox_binary.rb'), <<~RUBY) if firefox_binary
|
|
502
|
+
# frozen_string_literal: true
|
|
503
|
+
|
|
504
|
+
Selenium::WebDriver::Firefox.path = ENV['FIREFOX_BINARY'] if ENV['FIREFOX_BINARY']
|
|
505
|
+
RUBY
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def results(project)
|
|
509
|
+
JSON.parse(File.read(File.join(project, 'raider-results', 'run.json')))['results']
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def video_files(project)
|
|
513
|
+
Dir.glob(File.join(project, 'raider-results', '**', '*.{webm,mp4}'))
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def video_attachments(result)
|
|
517
|
+
result['attachments'].select { |attachment| attachment['name'] == 'video' }
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def expect_video_for_failure_only(project, failing_name, run)
|
|
521
|
+
by_status = results(project).group_by { |r| r['status'] }
|
|
522
|
+
expect(by_status.keys).to match_array(%w[passed failed]), "STDOUT: #{run[:stdout]}\nSTDERR: #{run[:stderr]}"
|
|
523
|
+
failing = by_status['failed'].find { |r| r['full_name'].include?(failing_name) }
|
|
524
|
+
expect(failing).not_to be_nil, "No failing result named #{failing_name}"
|
|
525
|
+
expect(video_attachments(failing).size).to eq(1)
|
|
526
|
+
expect(video_attachments(failing).first['content_type']).to eq('video/webm')
|
|
527
|
+
by_status.fetch('passed', []).each { |r| expect(video_attachments(r)).to be_empty }
|
|
528
|
+
videos = video_files(project)
|
|
529
|
+
expect(videos.size).to eq(1)
|
|
530
|
+
expect(videos.first).to end_with('video.webm')
|
|
531
|
+
expect(File.size(videos.first)).to be_positive
|
|
532
|
+
expect(File.read(File.join(project, 'raider-results', 'index.html'))).to include('<video')
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
describe 'Selenium + RSpec on headless Chrome' do
|
|
536
|
+
let(:project) { 'video_rspec_selenium' }
|
|
537
|
+
let(:command) { 'bundle exec rspec spec/video_replay_spec.rb' }
|
|
538
|
+
|
|
539
|
+
before do
|
|
540
|
+
skip_unless_video_prerequisites('chrome')
|
|
541
|
+
generate_video_project('rspec', 'selenium')
|
|
542
|
+
add_rspec_video_spec(project)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
after { FileUtils.rm_f(File.join(project, 'spec/video_replay_spec.rb')) }
|
|
546
|
+
|
|
547
|
+
it 'attaches video.webm to the failing example only and plays it in index.html', :aggregate_failures do
|
|
548
|
+
result = run_in_project(project, command, env: video_env)
|
|
549
|
+
expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
550
|
+
expect(result[:stderr]).not_to include('[video]')
|
|
551
|
+
expect_video_for_failure_only(project, 'fails after driving the page', result)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
it 'writes no video with VIDEO=off', :aggregate_failures do
|
|
555
|
+
result = run_in_project(project, command, env: video_env('VIDEO' => 'off'))
|
|
556
|
+
expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
557
|
+
expect(video_files(project)).to be_empty
|
|
558
|
+
expect(results(project).flat_map { |r| video_attachments(r) }).to be_empty
|
|
559
|
+
expect(result[:stderr]).not_to include('[video]')
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it 'runs unchanged with one [video] line and no download when no ffmpeg is usable', :aggregate_failures do
|
|
563
|
+
off = run_in_project(project, command, env: video_env('VIDEO' => 'off'))
|
|
564
|
+
FileUtils.rm_rf(File.join(project, 'raider-results'))
|
|
565
|
+
result = run_in_project(project, command, env: video_env('RAIDER_FFMPEG_PATH' => nil))
|
|
566
|
+
|
|
567
|
+
expect(result[:exit_code]).to eq(off[:exit_code]), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
568
|
+
video_lines = result[:stderr].lines.grep(/\[video\]/)
|
|
569
|
+
expect(video_lines.size).to eq(1), "STDERR: #{result[:stderr]}"
|
|
570
|
+
# Decided at the first driver/recorder call, before any capture or fetch: the placeholder
|
|
571
|
+
# manifest has no checksum, so nothing is captured or downloaded.
|
|
572
|
+
expect(video_lines.first).to include('no ffmpeg available for this platform yet')
|
|
573
|
+
expect(video_files(project)).to be_empty
|
|
574
|
+
expect(Dir.glob(File.join(cache_dir, '**', '*'))).to be_empty
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
describe 'Watir + Cucumber on headless Firefox' do
|
|
579
|
+
let(:project) { 'video_cucumber_watir' }
|
|
580
|
+
let(:firefox_binary) { ENV.fetch('FIREFOX_BINARY', nil) }
|
|
581
|
+
|
|
582
|
+
before do
|
|
583
|
+
skip_unless_video_prerequisites('firefox')
|
|
584
|
+
generate_video_project('cucumber', 'watir')
|
|
585
|
+
add_cucumber_video_feature(project, firefox_binary)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
after do
|
|
589
|
+
FileUtils.rm_f(File.join(project, 'features/video_replay.feature'))
|
|
590
|
+
FileUtils.rm_f(File.join(project, 'features/step_definitions/video_replay_steps.rb'))
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
it 'attaches video.webm to the failing scenario only and plays it in index.html', :aggregate_failures do
|
|
594
|
+
result = run_in_project(project, 'bundle exec cucumber features/video_replay.feature',
|
|
595
|
+
env: video_env('FIREFOX_BINARY' => firefox_binary), timeout: 240)
|
|
596
|
+
expect(result[:exit_code]).to eq(1), "STDOUT: #{result[:stdout]}\nSTDERR: #{result[:stderr]}"
|
|
597
|
+
expect(result[:stderr]).not_to include('[video]')
|
|
598
|
+
expect_video_for_failure_only(project, 'A failing scenario', result)
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
354
603
|
# Mobile and Visual frameworks require external services, so we only verify structure
|
|
355
604
|
context 'Mobile Automation Frameworks (structure validation only)' do
|
|
356
605
|
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
|