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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +18 -29
  3. data/.github/workflows/security.yml +68 -0
  4. data/.rubocop.yml +28 -0
  5. data/README.md +22 -4
  6. data/docs/superpowers/plans/2026-10-25-failure-video-replay.md +400 -0
  7. data/docs/superpowers/plans/2026-10-26-parity-cleanup.md +233 -0
  8. data/docs/superpowers/plans/2026-10-28-failure-card.md +295 -0
  9. data/docs/superpowers/specs/2026-10-25-failure-video-replay-design.md +214 -0
  10. data/docs/superpowers/specs/2026-10-28-failure-card-design.md +210 -0
  11. data/lib/commands/scaffolding_commands.rb +3 -3
  12. data/lib/commands/utility_commands.rb +36 -6
  13. data/lib/generators/cucumber/templates/cucumber.tt +1 -1
  14. data/lib/generators/cucumber/templates/partials/appium_env.tt +6 -0
  15. data/lib/generators/cucumber/templates/partials/selenium_env.tt +6 -0
  16. data/lib/generators/cucumber/templates/partials/watir_env.tt +6 -0
  17. data/lib/generators/helper_generator.rb +5 -1
  18. data/lib/generators/infrastructure/azure_generator.rb +1 -0
  19. data/lib/generators/infrastructure/github_generator.rb +1 -0
  20. data/lib/generators/infrastructure/templates/azure_pipelines.tt +26 -2
  21. data/lib/generators/infrastructure/templates/github.tt +31 -1
  22. data/lib/generators/infrastructure/templates/github_appium.tt +50 -1
  23. data/lib/generators/invoke_generators.rb +20 -2
  24. data/lib/generators/rspec/rspec_generator.rb +0 -1
  25. data/lib/generators/templates/common/gemfile.tt +1 -1
  26. data/lib/generators/templates/common/git_ignore.tt +3 -0
  27. data/lib/generators/templates/common/partials/mobile_config.tt +5 -0
  28. data/lib/generators/templates/common/partials/web_config.tt +6 -1
  29. data/lib/generators/templates/common/read_me.tt +80 -2
  30. data/lib/generators/templates/helpers/browser_helper.tt +5 -1
  31. data/lib/generators/templates/helpers/driver_helper.tt +1 -0
  32. data/lib/generators/templates/helpers/partials/selenium_driver.tt +4 -1
  33. data/lib/generators/templates/helpers/spec_helper.tt +8 -0
  34. data/lib/generators/templates/helpers/video_helper.tt +338 -171
  35. data/lib/ruby_raider/ffmpeg.rb +280 -0
  36. data/lib/ruby_raider.rb +2 -3
  37. data/lib/scaffolding/project_detector.rb +1 -3
  38. data/lib/scaffolding/scaffolding.rb +0 -1
  39. data/lib/utilities/utilities.rb +15 -2
  40. data/lib/version +1 -1
  41. data/ruby_raider.gemspec +3 -1
  42. data/sig/commands/utility_commands.rbs +4 -1
  43. data/sig/generators/invoke_generators.rbs +2 -3
  44. data/sig/ruby_raider/ffmpeg.rbs +23 -0
  45. data/sig/utilities/utilities.rbs +1 -0
  46. data/spec/commands/raider_commands_spec.rb +56 -1
  47. data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +28 -0
  48. data/spec/generators/video_helper_spec.rb +608 -0
  49. data/spec/integration/commands/browser_update_after_creation_spec.rb +7 -6
  50. data/spec/integration/commands/utility_commands_spec.rb +2 -2
  51. data/spec/integration/content/azure_content_spec.rb +48 -3
  52. data/spec/integration/content/ci_content_spec.rb +115 -5
  53. data/spec/integration/content/common_content_spec.rb +8 -0
  54. data/spec/integration/content/config_content_spec.rb +14 -0
  55. data/spec/integration/content/cucumber_reporter_content_spec.rb +22 -1
  56. data/spec/integration/content/gemfile_content_spec.rb +2 -3
  57. data/spec/integration/content/helper_content_spec.rb +146 -0
  58. data/spec/integration/content/page_content_spec.rb +0 -1
  59. data/spec/integration/content/readme_content_spec.rb +65 -0
  60. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  61. data/spec/integration/content/test_content_spec.rb +2 -3
  62. data/spec/integration/end_to_end_features_spec.rb +0 -2
  63. data/spec/integration/end_to_end_spec.rb +252 -26
  64. data/spec/integration/generators/azure_generator_spec.rb +7 -6
  65. data/spec/integration/generators/common_generator_spec.rb +1 -1
  66. data/spec/integration/generators/invoke_generators_spec.rb +52 -0
  67. data/spec/integration/scaffolding_e2e_spec.rb +13 -24
  68. data/spec/scaffolding/scaffold_project_detector_spec.rb +0 -1
  69. data/spec/scaffolding/scaffolding_features_spec.rb +0 -1
  70. data/spec/system/selenium_spec.rb +0 -1
  71. data/spec/system/watir_spec.rb +0 -1
  72. data/spec/utilities/ffmpeg_spec.rb +424 -0
  73. data/spec/utilities/utilities_spec.rb +19 -2
  74. data/spec/utilities/video_command_spec.rb +120 -0
  75. metadata +49 -11
  76. data/lib/commands/adopt_commands.rb +0 -42
  77. data/lib/commands/loaded_commands.rb +0 -9
  78. data/lib/commands/plugin_commands.rb +0 -32
  79. data/sig/commands/adopt_commands.rbs +0 -8
  80. data/sig/commands/loaded_commands.rbs +0 -5
  81. data/sig/commands/plugin_commands.rbs +0 -9
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'net/http'
6
+ require 'rbconfig'
7
+ require 'securerandom'
8
+ require 'uri'
9
+
10
+ module RubyRaider
11
+ # Pinned, checksum-verified ffmpeg provisioning. Stdlib only so generated
12
+ # projects can load it standalone via `require 'ruby_raider/ffmpeg'`.
13
+ module Ffmpeg
14
+ MAX_REDIRECTS = 5
15
+ # Seconds for the whole download, redirects included.
16
+ DOWNLOAD_TIMEOUT = 300
17
+
18
+ # Checksums are filled in when the RaiderHQ `ffmpeg-7.1` release is cut.
19
+ RELEASE_BASE = 'https://github.com/RaiderHQ/ruby_raider/releases/download/ffmpeg-7.1/ffmpeg-7.1-'
20
+ MANIFEST = {
21
+ version: '7.1',
22
+ assets: {
23
+ 'darwin-arm64' => { url: "#{RELEASE_BASE}darwin-arm64".freeze, sha256: nil }.freeze,
24
+ 'darwin-x64' => { url: "#{RELEASE_BASE}darwin-x64".freeze, sha256: nil }.freeze,
25
+ 'linux-x64' => { url: "#{RELEASE_BASE}linux-x64".freeze, sha256: nil }.freeze,
26
+ 'windows-x64' => { url: "#{RELEASE_BASE}windows-x64.exe".freeze, sha256: nil }.freeze
27
+ }.freeze
28
+ }.freeze
29
+
30
+ PLATFORMS = %w[darwin-arm64 darwin-x64 linux-x64 windows-x64].freeze
31
+ # $stderr.puts, not Kernel#warn: warn is silenced under -W0 / $VERBOSE = nil.
32
+ # rubocop:disable Style/StderrPuts
33
+ DEFAULT_WARN = ->(message) { $stderr.puts("[video] #{message}") }
34
+ # rubocop:enable Style/StderrPuts
35
+
36
+ @verified = {}
37
+
38
+ class << self
39
+ def platform(host_os: RbConfig::CONFIG['host_os'], host_cpu: RbConfig::CONFIG['host_cpu'])
40
+ os = host_os_name(host_os)
41
+ cpu = host_cpu_name(host_cpu)
42
+ return nil unless os && cpu
43
+
44
+ candidate = "#{os}-#{cpu}"
45
+ PLATFORMS.include?(candidate) ? candidate : nil
46
+ end
47
+
48
+ def cache_dir(env: ENV, platform: self.platform)
49
+ override = env['RAIDER_CACHE_DIR']
50
+ return override if override && !override.empty?
51
+
52
+ local = env['LOCALAPPDATA']
53
+ if platform == 'windows-x64' && local && !local.empty?
54
+ File.join(local, 'raider')
55
+ else
56
+ File.join(Dir.home, '.cache', 'raider')
57
+ end
58
+ end
59
+
60
+ def cached_path(env: ENV, manifest: MANIFEST, platform: self.platform)
61
+ name = platform.to_s.start_with?('windows') ? 'ffmpeg.exe' : 'ffmpeg'
62
+ File.join(cache_dir(env:, platform:), 'ffmpeg', manifest[:version], name)
63
+ end
64
+
65
+ # No network: the env override if executable, else the verified cached binary.
66
+ # Never raises: any StandardError becomes one warning and nil.
67
+ def resolve(env: ENV, manifest: MANIFEST, warn: nil, platform: nil)
68
+ warn ||= DEFAULT_WARN
69
+ guarded(warn) do
70
+ lookup(env, manifest, warn, platform || self.platform).first
71
+ end
72
+ end
73
+
74
+ # Never raises: any StandardError becomes one warning and nil.
75
+ def ensure!(env: ENV, manifest: MANIFEST, fetch: nil, warn: nil, platform: nil)
76
+ warn ||= DEFAULT_WARN
77
+ guarded(warn) do
78
+ platform ||= self.platform
79
+ path, final = lookup(env, manifest, warn, platform)
80
+ next path if path || final
81
+
82
+ provision(env, manifest, fetch || method(:http_get), warn, platform)
83
+ end
84
+ end
85
+
86
+ # Cheap check, no network and no hashing: can this run get an ffmpeg at all?
87
+ # True for an executable RAIDER_FFMPEG_PATH or a checksummed asset for the platform.
88
+ def obtainable?(env: ENV, manifest: MANIFEST, platform: nil)
89
+ override = env['RAIDER_FFMPEG_PATH']
90
+ return true if override && !override.empty? && executable_file?(override)
91
+
92
+ !manifest[:assets][(platform || self.platform).to_s]&.fetch(:sha256, nil).nil?
93
+ rescue StandardError
94
+ false
95
+ end
96
+
97
+ def http_get(url, destination, limit = MAX_REDIRECTS, timeout: DOWNLOAD_TIMEOUT, deadline: nil)
98
+ raise ArgumentError, "refusing non-https url: #{url}" unless https?(url)
99
+ raise IOError, 'too many redirects' if limit.negative?
100
+
101
+ deadline ||= monotonic_now + timeout
102
+ uri = URI(url)
103
+ location = nil
104
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, **socket_timeouts(deadline, timeout)) do |http|
105
+ http.request(Net::HTTP::Get.new(uri)) do |response|
106
+ location = handle_response(response, destination, deadline, timeout)
107
+ end
108
+ end
109
+ http_get(URI.join(url, location).to_s, destination, limit - 1, timeout:, deadline:) if location
110
+ end
111
+
112
+ private
113
+
114
+ # Returns the redirect location, or nil once the body is written.
115
+ def handle_response(response, destination, deadline, timeout)
116
+ case response
117
+ when Net::HTTPSuccess
118
+ File.open(destination, 'wb') do |file|
119
+ response.read_body do |chunk|
120
+ check_deadline(deadline, timeout)
121
+ file.write(chunk)
122
+ end
123
+ end
124
+ nil
125
+ when Net::HTTPRedirection
126
+ response['location'] || raise(IOError, 'redirect without Location header')
127
+ else
128
+ raise IOError, "download failed: HTTP #{response.code}"
129
+ end
130
+ end
131
+
132
+ def socket_timeouts(deadline, timeout)
133
+ left = check_deadline(deadline, timeout)
134
+ { open_timeout: [10, left].min, read_timeout: [60, left].min }
135
+ end
136
+
137
+ def check_deadline(deadline, timeout)
138
+ left = deadline - monotonic_now
139
+ raise IOError, "download took longer than #{timeout} seconds" unless left.positive?
140
+
141
+ left
142
+ end
143
+
144
+ def monotonic_now
145
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+ end
147
+
148
+ def guarded(warn)
149
+ yield
150
+ rescue StandardError => e
151
+ warn_nil(warn, "ffmpeg unavailable: #{e.message}")
152
+ end
153
+
154
+ def reset_verified_cache!
155
+ @verified.clear
156
+ end
157
+
158
+ def host_os_name(host_os)
159
+ case host_os
160
+ when /darwin/ then 'darwin'
161
+ when /linux/ then 'linux'
162
+ when /mswin|mingw|cygwin|windows/ then 'windows'
163
+ end
164
+ end
165
+
166
+ def host_cpu_name(host_cpu)
167
+ case host_cpu
168
+ when /arm64|aarch64/ then 'arm64'
169
+ when /x86_64|x64|amd64/ then 'x64'
170
+ end
171
+ end
172
+
173
+ # Returns [path_or_nil, final]. final means "do not try to download".
174
+ def lookup(env, manifest, warn, platform)
175
+ override = env['RAIDER_FFMPEG_PATH']
176
+ if override && !override.empty?
177
+ return [override, true] if executable_file?(override)
178
+
179
+ warn.call('RAIDER_FFMPEG_PATH is not an executable file; ignoring')
180
+ end
181
+ cached_lookup(env, manifest, warn, platform)
182
+ end
183
+
184
+ def cached_lookup(env, manifest, warn, platform)
185
+ sha = manifest[:assets][platform.to_s]&.fetch(:sha256, nil)
186
+ path = cached_path(env:, manifest:, platform:)
187
+ return [nil, false] unless platform && sha && executable_file?(path)
188
+ return [path, true] if verified_cached?(path, sha)
189
+
190
+ FileUtils.rm_f(path)
191
+ warn.call('cached ffmpeg failed checksum; video disabled')
192
+ [nil, true]
193
+ end
194
+
195
+ def verified_cached?(path, sha256)
196
+ key = verification_key(path, sha256)
197
+ return true if @verified[key]
198
+
199
+ ok = verified?(path, sha256)
200
+ @verified[key] = true if ok
201
+ ok
202
+ end
203
+
204
+ def verification_key(path, sha256)
205
+ stat = File.stat(path)
206
+ [path, stat.ino, stat.mtime.to_f, stat.ctime.to_f, stat.size, sha256]
207
+ end
208
+
209
+ def provision(env, manifest, fetch, warn, platform)
210
+ asset = manifest[:assets][platform.to_s]
211
+ unless asset
212
+ label = platform || "#{RbConfig::CONFIG['host_os']}/#{RbConfig::CONFIG['host_cpu']}"
213
+ return warn_nil(warn, "no ffmpeg build for this platform (#{label})")
214
+ end
215
+ return warn_nil(warn, 'ffmpeg build not published yet') unless asset[:sha256]
216
+ return warn_nil(warn, "refusing non-https ffmpeg url: #{asset[:url]}") unless https?(asset[:url])
217
+
218
+ download(cached_path(env:, manifest:, platform:), asset, fetch, warn)
219
+ end
220
+
221
+ def download(destination, asset, fetch, warn)
222
+ part = "#{destination}.#{Process.pid}.#{SecureRandom.hex(4)}.part"
223
+ created = missing_dirs(File.dirname(destination))
224
+ begin
225
+ FileUtils.mkdir_p(File.dirname(destination))
226
+ fetch.call(asset[:url], part)
227
+ return warn_nil(warn, "ffmpeg checksum mismatch for #{asset[:url]}; download discarded") unless verified?(part, asset[:sha256])
228
+
229
+ File.chmod(0o755, part)
230
+ # Rename wins: if another process already installed a valid binary, keep it.
231
+ File.rename(part, destination) unless File.file?(destination) && verified?(destination, asset[:sha256])
232
+ @verified[verification_key(destination, asset[:sha256])] = true
233
+ destination
234
+ rescue StandardError => e
235
+ warn_nil(warn, "ffmpeg download failed: #{e.message}")
236
+ ensure
237
+ FileUtils.rm_f(part)
238
+ remove_empty(created)
239
+ end
240
+ end
241
+
242
+ # The dirs mkdir_p would create for dir, deepest first.
243
+ def missing_dirs(dir)
244
+ missing = []
245
+ until File.exist?(dir) || File.dirname(dir) == dir
246
+ missing << dir
247
+ dir = File.dirname(dir)
248
+ end
249
+ missing
250
+ end
251
+
252
+ # After a failed download, so CI never caches an empty ffmpeg/<version>/.
253
+ # Dir.rmdir only removes empty dirs, so a concurrent install is kept.
254
+ def remove_empty(dirs)
255
+ dirs.each { |dir| Dir.rmdir(dir) }
256
+ rescue SystemCallError
257
+ nil
258
+ end
259
+
260
+ def verified?(path, sha256)
261
+ Digest::SHA256.file(path).hexdigest.casecmp?(sha256)
262
+ end
263
+
264
+ def executable_file?(path)
265
+ File.file?(path) && File.executable?(path)
266
+ end
267
+
268
+ def https?(url)
269
+ URI(url).scheme == 'https'
270
+ rescue URI::InvalidURIError
271
+ false
272
+ end
273
+
274
+ def warn_nil(warn, message)
275
+ warn.call(message)
276
+ nil
277
+ end
278
+ end
279
+ end
280
+ end
data/lib/ruby_raider.rb CHANGED
@@ -8,6 +8,8 @@ autoload :UtilityCommands, File.expand_path('commands/utility_commands', __dir__
8
8
 
9
9
  module RubyRaider
10
10
  class Raider < Thor
11
+ def self.exit_on_failure? = true
12
+
11
13
  no_tasks do
12
14
  def current_version = File.read(File.expand_path('version', __dir__)).strip
13
15
  end
@@ -52,9 +54,6 @@ module RubyRaider
52
54
  no_tasks do
53
55
  def generate_from_parameters(params)
54
56
  InvokeGenerators.generate_framework(params)
55
- rescue InvokeGenerators::InvalidParameters => e
56
- puts "Invalid parameters: #{e.message}"
57
- exit 1
58
57
  end
59
58
 
60
59
  def merge_flags(params)
@@ -66,9 +66,7 @@ module ScaffoldProjectDetector
66
66
  def validate_project
67
67
  warnings = []
68
68
  warnings << 'Gemfile not found. Are you in a Ruby Raider project directory?' unless File.exist?('Gemfile')
69
- unless File.exist?(CONFIG_PATH)
70
- warnings << "#{CONFIG_PATH} not found. Scaffolded pages won't include URL navigation methods."
71
- end
69
+ warnings << "#{CONFIG_PATH} not found. Scaffolded pages won't include URL navigation methods." unless File.exist?(CONFIG_PATH)
72
70
  warnings
73
71
  end
74
72
 
@@ -95,5 +95,4 @@ class Scaffolding < Thor::Group
95
95
  def default_path(standard_path, file_type)
96
96
  path ? "#{path}/#{normalized_name}#{file_type}" : standard_path
97
97
  end
98
-
99
98
  end
@@ -7,7 +7,12 @@ module Utilities
7
7
 
8
8
  class << self
9
9
  def browser=(browser)
10
- set('browser', browser.to_s.delete_prefix(':'))
10
+ reload_config
11
+ name = browser.to_s.delete_prefix(':')
12
+ @config['browser'] = name
13
+ @config['browser_arguments'] ||= {}
14
+ @config['browser_arguments'][name] ||= []
15
+ overwrite_yaml
11
16
  end
12
17
 
13
18
  def page_path=(path)
@@ -54,7 +59,8 @@ module Utilities
54
59
  def delete_browser_options
55
60
  reload_config
56
61
  browser_name = @config['browser'] || 'chrome'
57
- @config['browser_arguments']&.delete(browser_name)
62
+ @config['browser_arguments'] ||= {}
63
+ @config['browser_arguments'][browser_name] = []
58
64
  overwrite_yaml
59
65
  end
60
66
 
@@ -62,6 +68,13 @@ module Utilities
62
68
  set('headless', enabled)
63
69
  end
64
70
 
71
+ def video_enabled=(enabled)
72
+ reload_config
73
+ video = @config['video']
74
+ @config['video'] = (video.is_a?(Hash) ? video : {}).merge('enabled' => enabled)
75
+ overwrite_yaml
76
+ end
77
+
65
78
  # Set multiple config keys in a single YAML write.
66
79
  # Usage: Utilities.batch_update(browser: 'chrome', timeout: 30)
67
80
  def batch_update(**settings)
data/lib/version CHANGED
@@ -1 +1 @@
1
- 3.8.0
1
+ 3.10.0
data/ruby_raider.gemspec CHANGED
@@ -13,12 +13,14 @@ Gem::Specification.new do |s|
13
13
  s.files = `git ls-files -z`.split("\x0")
14
14
  s.bindir = 'bin'
15
15
  s.executables << 'raider'
16
+ s.add_development_dependency 'bundler-audit', '~> 0.9'
16
17
  s.add_development_dependency 'rake', '~> 13.0.6'
17
- s.add_development_dependency 'reek', '~> 6.1.0'
18
+ s.add_development_dependency 'reek', '~> 6.5'
18
19
  s.add_development_dependency 'rspec', '~> 3.11.0'
19
20
  s.add_development_dependency 'rubocop', '~> 1.27'
20
21
  s.add_development_dependency 'rubocop-performance', '~> 1.15.0'
21
22
  s.add_development_dependency 'rubocop-rspec', '~> 2.9.0'
23
+ s.add_development_dependency 'ruby_audit', '~> 3.1'
22
24
  s.add_development_dependency 'steep', '~> 1.9'
23
25
 
24
26
  s.add_runtime_dependency 'thor', '~> 1.2.1'
@@ -5,10 +5,13 @@ class UtilityCommands < Thor
5
5
  def browser: (?String? default_browser) -> void
6
6
  def browser_options: (*String opts) -> void
7
7
  def raid: () -> void
8
- def config: () -> void
9
8
  def timeout: (String seconds) -> void
10
9
  def viewport: (String dimensions) -> void
11
10
  def platform: (String platform) -> void
12
11
  def start_appium: () -> void
13
12
  def desktop: () -> void
13
+ def video: (String action) -> void
14
+ def video_install: () -> void
15
+ def video_status: () -> void
16
+ def video_toggle: (bool enabled) -> void
14
17
  end
@@ -1,9 +1,8 @@
1
1
  # Type definitions for InvokeGenerators module
2
2
  module InvokeGenerators
3
- class InvalidParameters < ArgumentError
4
- end
5
-
6
3
  def self?.generate_framework: (?Hash[Symbol, untyped] structure) -> void
4
+ def self?.normalise_options: (Hash[Symbol, untyped] structure) -> Hash[Symbol, untyped]
5
+ def self?.validate_option: (Symbol key, untyped value, Array[String] allowed) -> void
7
6
  def self?.validate_ci: (untyped provider) -> void
8
7
  def self?.print_mobile_azure_notice: (Hash[Symbol, untyped] structure) -> void
9
8
  def self?.add_generator: (Array[String] generators, *String gens) -> void
@@ -0,0 +1,23 @@
1
+ module RubyRaider
2
+ module Ffmpeg
3
+ type asset = { url: String, sha256: String? }
4
+ type manifest = { version: String, assets: Hash[String, asset] }
5
+
6
+ MAX_REDIRECTS: Integer
7
+ DOWNLOAD_TIMEOUT: Integer
8
+ RELEASE_BASE: String
9
+ MANIFEST: manifest
10
+ PLATFORMS: Array[String]
11
+ DEFAULT_WARN: ^(String) -> void
12
+
13
+ self.@verified: Hash[Array[untyped], bool]
14
+
15
+ def self.platform: (?host_os: String, ?host_cpu: String) -> String?
16
+ def self.cache_dir: (?env: Hash[String, String], ?platform: String?) -> String
17
+ def self.cached_path: (?env: Hash[String, String], ?manifest: manifest, ?platform: String?) -> String
18
+ def self.resolve: (?env: Hash[String, String], ?manifest: manifest, ?warn: (^(String) -> void)?, ?platform: String?) -> String?
19
+ def self.ensure!: (?env: Hash[String, String], ?manifest: manifest, ?fetch: (^(String, String) -> untyped)?, ?warn: (^(String) -> void)?, ?platform: String?) -> String?
20
+ def self.obtainable?: (?env: Hash[String, String], ?manifest: manifest, ?platform: String?) -> bool
21
+ def self.http_get: (String url, String destination, ?Integer limit, ?timeout: Numeric, ?deadline: Float?) -> void
22
+ end
23
+ end
@@ -13,6 +13,7 @@ module Utilities
13
13
  def self?.platform=: (String platform) -> void
14
14
  def self?.browser_options=: (Array[String] opts) -> void
15
15
  def self?.delete_browser_options: () -> void
16
+ def self?.video_enabled=: (bool enabled) -> void
16
17
  def self?.batch_update: (**untyped settings) -> void
17
18
  def self?.run: (?String? opts) -> void
18
19
  def self?.parallel_run: (?String? opts, ?untyped? _settings) -> void
@@ -3,7 +3,8 @@
3
3
  require 'rspec'
4
4
  require_relative '../../lib/ruby_raider'
5
5
 
6
- RSpec.describe RubyRaider::Raider do
6
+ # Command specs are grouped by CLI area under spec/commands, not by constant path
7
+ RSpec.describe RubyRaider::Raider do # rubocop:disable RSpec/FilePath
7
8
  describe 'new command' do
8
9
  context 'with parameters' do
9
10
  it 'calls InvokeGenerators.generate_framework with parsed hash' do
@@ -65,4 +66,58 @@ RSpec.describe RubyRaider::Raider do
65
66
  expect(subcommands).to include('utility')
66
67
  end
67
68
  end
69
+
70
+ describe 'CLI exit status' do
71
+ require 'open3'
72
+ require 'tmpdir'
73
+
74
+ let(:raider_bin) { File.expand_path('../../bin/raider', __dir__) }
75
+
76
+ def run_raider(*args, chdir: Dir.pwd)
77
+ Open3.capture3(RbConfig.ruby, raider_bin, *args, chdir:)
78
+ end
79
+
80
+ it 'exits non-zero for an unknown command' do
81
+ _out, err, status = run_raider('u', 'debug', 'on')
82
+
83
+ expect(status.exitstatus).not_to eq(0)
84
+ expect(err).to include('Could not find command "debug"')
85
+ end
86
+
87
+ it 'exits zero for version' do
88
+ _out, _err, status = run_raider('version')
89
+
90
+ expect(status.exitstatus).to eq(0)
91
+ end
92
+
93
+ it 'rejects an unknown framework without creating the project' do
94
+ Dir.mktmpdir do |dir|
95
+ out, err, status = run_raider('new', 'x', '-p', 'framework:minitest', 'automation:selenium', chdir: dir)
96
+
97
+ expect(status.exitstatus).not_to eq(0)
98
+ expect(out + err).to include('framework', 'rspec', 'cucumber')
99
+ expect(File.exist?(File.join(dir, 'x'))).to be(false)
100
+ end
101
+ end
102
+
103
+ it 'rejects an unknown automation without creating the project' do
104
+ Dir.mktmpdir do |dir|
105
+ out, err, status = run_raider('new', 'x', '-p', 'framework:rspec', 'automation:playwright', chdir: dir)
106
+
107
+ expect(status.exitstatus).not_to eq(0)
108
+ expect(out + err).to include('automation', 'selenium', 'cross_platform')
109
+ expect(File.exist?(File.join(dir, 'x'))).to be(false)
110
+ end
111
+ end
112
+
113
+ it 'rejects automation:appium and points to the mobile platforms' do
114
+ Dir.mktmpdir do |dir|
115
+ out, err, status = run_raider('new', 'x', '-p', 'framework:rspec', 'automation:appium', chdir: dir)
116
+
117
+ expect(status.exitstatus).not_to eq(0)
118
+ expect(out + err).to include('automation', 'ios', 'android', 'cross_platform')
119
+ expect(File.exist?(File.join(dir, 'x'))).to be(false)
120
+ end
121
+ end
122
+ end
68
123
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Minimal stand-in for selenium-webdriver, which is not in ruby_raider's bundle.
4
+ # spec/generators/video_helper_spec.rb puts this directory on $LOAD_PATH so the
5
+ # rendered video_helper.rb can `require 'selenium-webdriver'`.
6
+ module Selenium
7
+ module WebDriver
8
+ module Error
9
+ class WebDriverError < StandardError; end
10
+ class NoSuchAlertError < WebDriverError; end
11
+ end
12
+
13
+ module Support
14
+ class AbstractEventListener
15
+ def before_navigate_to(url, driver) end
16
+ def after_navigate_to(url, driver) end
17
+ def before_find(by, what, driver) end
18
+ def after_find(by, what, driver) end
19
+ def before_click(element, driver) end
20
+ def after_click(element, driver) end
21
+ def before_change_value_of(element, driver) end
22
+ def after_change_value_of(element, driver) end
23
+ def before_execute_script(script, driver) end
24
+ def after_execute_script(script, driver) end
25
+ end
26
+ end
27
+ end
28
+ end