ruby_raider 2.0.0 → 3.0.1

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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +58 -0
  3. data/.github/workflows/steep.yml +21 -0
  4. data/.gitignore +1 -1
  5. data/.reek.yml +46 -4
  6. data/.ruby-version +1 -1
  7. data/README.md +138 -77
  8. data/Steepfile +22 -0
  9. data/assets/ruby_raider_logo.svg +51 -0
  10. data/lib/adopter/adopt_menu.rb +11 -15
  11. data/lib/adopter/converters/base_converter.rb +1 -2
  12. data/lib/adopter/converters/identity_converter.rb +3 -6
  13. data/lib/adopter/migration_plan.rb +0 -1
  14. data/lib/adopter/plan_builder.rb +2 -5
  15. data/lib/adopter/project_analyzer.rb +1 -5
  16. data/lib/adopter/project_detector.rb +3 -5
  17. data/lib/commands/adopt_commands.rb +0 -1
  18. data/lib/commands/plugin_commands.rb +0 -2
  19. data/lib/commands/scaffolding_commands.rb +246 -38
  20. data/lib/commands/utility_commands.rb +92 -3
  21. data/lib/generators/automation/automation_generator.rb +0 -7
  22. data/lib/generators/automation/templates/partials/element.tt +1 -1
  23. data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -7
  24. data/lib/generators/automation/templates/partials/url_methods.tt +0 -1
  25. data/lib/generators/common_generator.rb +12 -0
  26. data/lib/generators/cucumber/cucumber_generator.rb +36 -0
  27. data/lib/generators/cucumber/templates/accessibility_feature.tt +5 -0
  28. data/lib/generators/cucumber/templates/accessibility_steps.tt +21 -0
  29. data/lib/generators/cucumber/templates/cucumber.tt +8 -1
  30. data/lib/generators/cucumber/templates/feature.tt +0 -4
  31. data/lib/generators/cucumber/templates/partials/appium_env.tt +5 -0
  32. data/lib/generators/cucumber/templates/partials/capybara_env.tt +19 -1
  33. data/lib/generators/cucumber/templates/partials/driver_world.tt +1 -4
  34. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -35
  35. data/lib/generators/cucumber/templates/partials/watir_env.tt +20 -1
  36. data/lib/generators/cucumber/templates/partials/web_steps.tt +6 -12
  37. data/lib/generators/cucumber/templates/performance_feature.tt +5 -0
  38. data/lib/generators/cucumber/templates/performance_steps.tt +17 -0
  39. data/lib/generators/cucumber/templates/visual_feature.tt +5 -0
  40. data/lib/generators/cucumber/templates/visual_steps.tt +19 -0
  41. data/lib/generators/generator.rb +38 -7
  42. data/lib/generators/helper_generator.rb +24 -7
  43. data/lib/generators/infrastructure/templates/github.tt +1 -1
  44. data/lib/generators/infrastructure/templates/github_appium.tt +2 -2
  45. data/lib/generators/infrastructure/templates/gitlab.tt +1 -1
  46. data/lib/generators/invoke_generators.rb +42 -9
  47. data/lib/generators/menu_generator.rb +120 -11
  48. data/lib/generators/minitest/minitest_generator.rb +16 -4
  49. data/lib/generators/minitest/templates/accessibility_test.tt +26 -0
  50. data/lib/generators/minitest/templates/performance_test.tt +18 -0
  51. data/lib/generators/minitest/templates/test.tt +5 -34
  52. data/lib/generators/minitest/templates/visual_test.tt +23 -0
  53. data/lib/generators/rspec/rspec_generator.rb +16 -4
  54. data/lib/generators/rspec/templates/accessibility_spec.tt +25 -0
  55. data/lib/generators/rspec/templates/performance_spec.tt +18 -0
  56. data/lib/generators/rspec/templates/spec.tt +5 -35
  57. data/lib/generators/rspec/templates/visual_spec.tt +20 -0
  58. data/lib/generators/template_renderer/partial_cache.rb +11 -1
  59. data/lib/generators/template_renderer/partial_resolver.rb +17 -10
  60. data/lib/generators/template_renderer.rb +17 -1
  61. data/lib/generators/templates/common/gemfile.tt +21 -6
  62. data/lib/generators/templates/common/git_ignore.tt +6 -1
  63. data/lib/generators/templates/common/partials/mobile_config.tt +5 -1
  64. data/lib/generators/templates/common/partials/web_config.tt +16 -7
  65. data/lib/generators/templates/common/rakefile.tt +36 -0
  66. data/lib/generators/templates/common/read_me.tt +41 -91
  67. data/lib/generators/templates/common/rspec.tt +3 -0
  68. data/lib/generators/templates/common/ruby_version.tt +1 -0
  69. data/lib/generators/templates/helpers/allure_helper.tt +11 -0
  70. data/lib/generators/templates/helpers/browser_helper.tt +13 -3
  71. data/lib/generators/templates/helpers/capybara_helper.tt +6 -2
  72. data/lib/generators/templates/helpers/debug_helper.tt +190 -0
  73. data/lib/generators/templates/helpers/driver_helper.tt +2 -10
  74. data/lib/generators/templates/helpers/partials/appium_driver.tt +0 -2
  75. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +7 -0
  76. data/lib/generators/templates/helpers/partials/debug_start.tt +7 -0
  77. data/lib/generators/templates/helpers/partials/driver_and_options.tt +1 -3
  78. data/lib/generators/templates/helpers/partials/selenium_driver.tt +9 -8
  79. data/lib/generators/templates/helpers/partials/video_start.tt +9 -0
  80. data/lib/generators/templates/helpers/partials/video_stop.tt +4 -0
  81. data/lib/generators/templates/helpers/performance_helper.tt +57 -0
  82. data/lib/generators/templates/helpers/spec_helper.tt +57 -8
  83. data/lib/generators/templates/helpers/test_helper.tt +69 -1
  84. data/lib/generators/templates/helpers/video_helper.tt +270 -0
  85. data/lib/generators/templates/helpers/visual_helper.tt +39 -46
  86. data/lib/llm/client.rb +79 -0
  87. data/lib/llm/config.rb +57 -0
  88. data/lib/llm/prompts.rb +84 -0
  89. data/lib/llm/provider.rb +27 -0
  90. data/lib/llm/providers/anthropic_provider.rb +43 -0
  91. data/lib/llm/providers/ollama_provider.rb +56 -0
  92. data/lib/llm/providers/openai_provider.rb +42 -0
  93. data/lib/llm/response_parser.rb +67 -0
  94. data/lib/plugin/plugin.rb +22 -20
  95. data/lib/plugin/plugin_exposer.rb +16 -38
  96. data/lib/ruby_raider.rb +47 -12
  97. data/lib/scaffolding/crud_generator.rb +94 -0
  98. data/lib/scaffolding/dry_run_presenter.rb +16 -0
  99. data/lib/scaffolding/name_normalizer.rb +63 -0
  100. data/lib/scaffolding/page_introspector.rb +45 -0
  101. data/lib/scaffolding/project_detector.rb +96 -0
  102. data/lib/scaffolding/scaffold_menu.rb +103 -0
  103. data/lib/scaffolding/scaffolding.rb +158 -11
  104. data/lib/scaffolding/templates/component.tt +30 -0
  105. data/lib/scaffolding/templates/feature.tt +4 -4
  106. data/lib/scaffolding/templates/helper.tt +15 -1
  107. data/lib/scaffolding/templates/page_from_url.tt +75 -0
  108. data/lib/scaffolding/templates/page_object.tt +50 -1
  109. data/lib/scaffolding/templates/spec.tt +33 -2
  110. data/lib/scaffolding/templates/spec_from_page.tt +31 -0
  111. data/lib/scaffolding/templates/spec_from_url.tt +46 -0
  112. data/lib/scaffolding/templates/steps.tt +17 -5
  113. data/lib/scaffolding/url_analyzer.rb +179 -0
  114. data/lib/utilities/desktop_downloader.rb +177 -0
  115. data/lib/utilities/logo.rb +83 -0
  116. data/lib/utilities/utilities.rb +61 -20
  117. data/lib/version +1 -1
  118. data/ruby_raider.gemspec +1 -0
  119. data/sig/adopter/adopt_menu.rbs +25 -0
  120. data/sig/adopter/converters/base_converter.rbs +23 -0
  121. data/sig/adopter/converters/identity_converter.rbs +16 -0
  122. data/sig/adopter/migration_plan.rbs +34 -0
  123. data/sig/adopter/migrator.rbs +21 -0
  124. data/sig/adopter/plan_builder.rbs +38 -0
  125. data/sig/adopter/project_analyzer.rbs +39 -0
  126. data/sig/adopter/project_detector.rbs +26 -0
  127. data/sig/commands/adopt_commands.rbs +8 -0
  128. data/sig/commands/loaded_commands.rbs +5 -0
  129. data/sig/commands/plugin_commands.rbs +9 -0
  130. data/sig/commands/scaffolding_commands.rbs +28 -0
  131. data/sig/commands/utility_commands.rbs +21 -0
  132. data/sig/generators/automation/automation_generator.rbs +20 -0
  133. data/sig/generators/common_generator.rbs +12 -0
  134. data/sig/generators/cucumber/cucumber_generator.rbs +16 -0
  135. data/sig/generators/generator.rbs +40 -0
  136. data/sig/generators/helper_generator.rbs +18 -0
  137. data/sig/generators/infrastructure/github_generator.rbs +5 -0
  138. data/sig/generators/infrastructure/gitlab_generator.rbs +4 -0
  139. data/sig/generators/invoke_generators.rbs +10 -0
  140. data/sig/generators/menu_generator.rbs +29 -0
  141. data/sig/generators/minitest/minitest_generator.rbs +8 -0
  142. data/sig/generators/rspec/rspec_generator.rbs +8 -0
  143. data/sig/generators/template_renderer/partial_cache.rbs +20 -0
  144. data/sig/generators/template_renderer/partial_resolver.rbs +20 -0
  145. data/sig/generators/template_renderer/template_error.rbs +19 -0
  146. data/sig/generators/template_renderer.rbs +10 -0
  147. data/sig/llm/client.rbs +15 -0
  148. data/sig/llm/config.rbs +20 -0
  149. data/sig/llm/prompts.rbs +8 -0
  150. data/sig/llm/provider.rbs +12 -0
  151. data/sig/llm/providers/anthropic_provider.rbs +16 -0
  152. data/sig/llm/providers/ollama_provider.rbs +18 -0
  153. data/sig/llm/providers/openai_provider.rbs +16 -0
  154. data/sig/llm/response_parser.rbs +13 -0
  155. data/sig/plugin/plugin.rbs +24 -0
  156. data/sig/plugin/plugin_exposer.rbs +20 -0
  157. data/sig/ruby_raider.rbs +15 -0
  158. data/sig/scaffolding/crud_generator.rbs +16 -0
  159. data/sig/scaffolding/dry_run_presenter.rbs +4 -0
  160. data/sig/scaffolding/name_normalizer.rbs +17 -0
  161. data/sig/scaffolding/page_introspector.rbs +14 -0
  162. data/sig/scaffolding/project_detector.rbs +14 -0
  163. data/sig/scaffolding/scaffold_menu.rbs +18 -0
  164. data/sig/scaffolding/scaffolding.rbs +55 -0
  165. data/sig/scaffolding/url_analyzer.rbs +28 -0
  166. data/sig/utilities/desktop_downloader.rbs +23 -0
  167. data/sig/utilities/logger.rbs +13 -0
  168. data/sig/utilities/logo.rbs +16 -0
  169. data/sig/utilities/utilities.rbs +30 -0
  170. data/sig/vendor/thor.rbs +34 -0
  171. data/sig/vendor/tty_prompt.rbs +15 -0
  172. data/spec/adopter/adopt_menu_spec.rb +12 -12
  173. data/spec/adopter/migration_plan_spec.rb +1 -1
  174. data/spec/adopter/migrator_spec.rb +2 -2
  175. data/spec/adopter/project_detector_spec.rb +1 -1
  176. data/spec/commands/raider_commands_spec.rb +129 -0
  177. data/spec/commands/scaffolding_commands_spec.rb +22 -0
  178. data/spec/generators/generator_spec.rb +23 -0
  179. data/spec/integration/commands/scaffolding_commands_spec.rb +1 -1
  180. data/spec/integration/commands/utility_commands_spec.rb +29 -9
  181. data/spec/integration/content/ci_content_spec.rb +119 -0
  182. data/spec/integration/content/common_content_spec.rb +288 -0
  183. data/spec/integration/content/config_content_spec.rb +175 -0
  184. data/spec/integration/content/content_helper.rb +32 -0
  185. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  186. data/spec/integration/content/helper_content_spec.rb +485 -0
  187. data/spec/integration/content/page_content_spec.rb +259 -0
  188. data/spec/integration/content/reporter_content_spec.rb +236 -0
  189. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  190. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  191. data/spec/integration/content/test_content_spec.rb +266 -0
  192. data/spec/integration/end_to_end_features_spec.rb +690 -0
  193. data/spec/integration/end_to_end_spec.rb +52 -16
  194. data/spec/integration/generators/automation_generator_spec.rb +0 -12
  195. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  196. data/spec/integration/generators/common_generator_spec.rb +12 -13
  197. data/spec/integration/generators/config_features_spec.rb +155 -0
  198. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  199. data/spec/integration/generators/helpers_generator_spec.rb +0 -12
  200. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  201. data/spec/integration/generators/minitest_generator_spec.rb +0 -6
  202. data/spec/integration/generators/reporter_spec.rb +159 -0
  203. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  204. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  205. data/spec/integration/scaffolding_e2e_spec.rb +775 -0
  206. data/spec/integration/settings_helper.rb +0 -3
  207. data/spec/integration/spec_helper.rb +30 -13
  208. data/spec/llm/client_spec.rb +79 -0
  209. data/spec/llm/config_spec.rb +92 -0
  210. data/spec/llm/prompts_spec.rb +49 -0
  211. data/spec/llm/response_parser_spec.rb +92 -0
  212. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  213. data/spec/menus/menu_generator_spec.rb +263 -0
  214. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  215. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  216. data/spec/scaffolding/scaffold_project_detector_spec.rb +142 -0
  217. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  218. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  219. data/spec/system/adopt_matrix_spec.rb +537 -0
  220. data/spec/system/adopt_spec.rb +225 -0
  221. data/spec/system/support/system_test_helper.rb +0 -2
  222. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  223. data/spec/utilities/headless_config_spec.rb +89 -0
  224. data/spec/utilities/utilities_spec.rb +105 -0
  225. metadata +154 -5
  226. data/lib/generators/automation/templates/visual_options.tt +0 -16
  227. data/lib/generators/templates/helpers/partials/axe_driver.tt +0 -10
  228. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -4,7 +4,6 @@ require_relative 'migration_plan'
4
4
  require_relative 'converters/identity_converter'
5
5
 
6
6
  module Adopter
7
- # :reek:TooManyMethods { enabled: false }
8
7
  class PlanBuilder
9
8
  def initialize(analysis, params)
10
9
  @analysis = analysis
@@ -63,7 +62,7 @@ module Adopter
63
62
  tests = @analysis[:tests] || []
64
63
  return [] if target_cucumber?
65
64
 
66
- tests.select { |t| t[:type] != :cucumber }.map do |test|
65
+ tests.reject { |t| t[:type] == :cucumber }.map do |test|
67
66
  source_file = File.join(@params[:source_path], test[:path])
68
67
  content = File.read(source_file)
69
68
  converted = convert_test(content, test)
@@ -87,7 +86,7 @@ module Adopter
87
86
 
88
87
  ConvertedFile.new(
89
88
  output_path: raider_feature_path(feature[:path]),
90
- content: content,
89
+ content:,
91
90
  source_file: feature[:path],
92
91
  conversion_notes: 'Feature file copied as-is'
93
92
  )
@@ -125,7 +124,6 @@ module Adopter
125
124
  end
126
125
  end
127
126
 
128
- # :reek:ControlParameter { enabled: false }
129
127
  def convert_page_dsl(content, page, source_dsl, target)
130
128
  converter = find_page_converter(source_dsl, target)
131
129
  if converter
@@ -162,7 +160,6 @@ module Adopter
162
160
  end
163
161
  end
164
162
 
165
- # :reek:ControlParameter { enabled: false }
166
163
  def convert_test_framework(content, test, source_framework, target)
167
164
  converter = find_test_converter(source_framework, target)
168
165
  if converter
@@ -5,7 +5,6 @@ require_relative 'project_detector'
5
5
  module Adopter
6
6
  class MobileProjectError < StandardError; end
7
7
 
8
- # :reek:TooManyMethods { enabled: false }
9
8
  class ProjectAnalyzer
10
9
  KNOWN_FRAMEWORK_GEMS = %w[
11
10
  activesupport allure-cucumber allure-rspec allure-minitest allure-ruby-commons
@@ -79,7 +78,6 @@ module Adopter
79
78
  'Mobile (Appium) projects cannot be adopted. Only web-based projects are supported.'
80
79
  end
81
80
 
82
- # :reek:FeatureEnvy { enabled: false }
83
81
  def discover_pages
84
82
  page_path = detect_page_dir
85
83
  return [] unless page_path
@@ -115,7 +113,6 @@ module Adopter
115
113
  results
116
114
  end
117
115
 
118
- # :reek:FeatureEnvy { enabled: false }
119
116
  def discover_helpers
120
117
  helper_files = Dir.glob(File.join(@source_path, '**', '*helper*.rb')) +
121
118
  Dir.glob(File.join(@source_path, '**', 'support', '*.rb')) +
@@ -129,7 +126,7 @@ module Adopter
129
126
 
130
127
  {
131
128
  path: relative_path(file),
132
- role: role,
129
+ role:,
133
130
  modules_defined: extract_modules(content)
134
131
  }
135
132
  end
@@ -153,7 +150,6 @@ module Adopter
153
150
  end
154
151
  end
155
152
 
156
- # :reek:NestedIterators { enabled: false }
157
153
  def discover_custom_gems
158
154
  gemfile = File.join(@source_path, 'Gemfile')
159
155
  return [] unless File.exist?(gemfile)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Adopter
4
- # :reek:TooManyMethods { enabled: false }
5
4
  module ProjectDetector
6
5
  GEM_AUTOMATION_MAP = {
7
6
  'site_prism' => 'capybara',
@@ -9,9 +8,9 @@ module Adopter
9
8
  'selenium-webdriver' => 'selenium',
10
9
  'watir' => 'watir',
11
10
  'appium_lib' => 'appium',
12
- 'eyes_selenium' => 'applitools',
13
- 'axe-core-selenium' => 'axe',
14
- 'axe-core-rspec' => 'axe'
11
+ 'eyes_selenium' => 'selenium',
12
+ 'axe-core-selenium' => 'selenium',
13
+ 'axe-core-rspec' => 'selenium'
15
14
  }.freeze
16
15
 
17
16
  GEM_FRAMEWORK_MAP = {
@@ -78,7 +77,6 @@ module Adopter
78
77
  find_existing_dir(path, candidates)
79
78
  end
80
79
 
81
- # :reek:NestedIterators { enabled: false }
82
80
  def detect_browser(path)
83
81
  config_files = helper_and_config_files(path)
84
82
  config_files.each do |file|
@@ -3,7 +3,6 @@
3
3
  require 'thor'
4
4
  require_relative '../adopter/adopt_menu'
5
5
 
6
- # :reek:FeatureEnvy { enabled: false }
7
6
  class AdoptCommands < Thor
8
7
  desc 'project [SOURCE_PATH]', 'Adopts an existing test project into Ruby Raider conventions'
9
8
  option :parameters,
@@ -4,8 +4,6 @@ require 'thor'
4
4
  require_relative '../plugin/plugin'
5
5
 
6
6
  module RubyRaider
7
- # :reek:FeatureEnvy { enabled: false }
8
- # :reek:UtilityFunction { enabled: false }
9
7
  class PluginCommands < Thor
10
8
  desc 'add [NAME]', 'Adds a plugin to your project'
11
9
 
@@ -1,100 +1,308 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'fileutils'
5
+ require 'pathname'
4
6
  require_relative '../generators/menu_generator'
5
7
  require_relative '../scaffolding/scaffolding'
8
+ require_relative '../scaffolding/name_normalizer'
9
+ require_relative '../scaffolding/dry_run_presenter'
10
+ require_relative '../scaffolding/project_detector'
6
11
  require_relative '../commands/utility_commands'
7
12
 
8
- # :reek:FeatureEnvy { enabled: false }
9
- # :reek:UtilityFunction { enabled: false }
10
- # :reek:RepeatedConditional { enabled: false }
11
13
  class ScaffoldingCommands < Thor
14
+ class_option :dry_run, type: :boolean, default: false,
15
+ desc: 'Preview files without creating them', banner: ''
16
+
12
17
  desc 'page [PAGE_NAME]', 'Creates a new page object'
13
- option :path,
14
- type: :string, required: false, desc: 'The path where your page will be created', aliases: '-p'
15
- option :delete,
16
- type: :boolean, required: false, desc: 'This will delete the selected page', aliases: '-d'
18
+ option :path, type: :string, required: false,
19
+ desc: 'The path where your page will be created', aliases: '-p'
20
+ option :delete, type: :boolean, required: false,
21
+ desc: 'This will delete the selected page', aliases: '-d'
22
+ option :uses, type: :array, required: false,
23
+ desc: 'Dependent pages to require', aliases: '-u'
17
24
 
18
25
  def page(name)
19
26
  return delete_scaffolding(name, 'page') if options[:delete]
27
+ return dry_run_preview(name, 'page') if options[:dry_run]
20
28
 
21
- generate_scaffolding(name, 'page', options[:path])
29
+ generate_scaffolding(name, 'page', options[:path], uses: options[:uses])
22
30
  end
23
31
 
24
32
  desc 'feature [NAME]', 'Creates a new feature'
25
- option :path,
26
- type: :string,
27
- required: false, desc: 'The path where your feature will be created', aliases: '-p'
28
- option :delete,
29
- type: :boolean,
30
- required: false, desc: 'This will delete the selected feature', aliases: '-d'
33
+ option :path, type: :string, required: false,
34
+ desc: 'The path where your feature will be created', aliases: '-p'
35
+ option :delete, type: :boolean, required: false,
36
+ desc: 'This will delete the selected feature', aliases: '-d'
31
37
 
32
38
  def feature(name)
33
39
  return delete_scaffolding(name, 'feature') if options[:delete]
40
+ return dry_run_preview(name, 'feature') if options[:dry_run]
34
41
 
35
42
  generate_scaffolding(name, 'feature', options[:path])
36
43
  end
37
44
 
38
45
  desc 'spec [SPEC_NAME]', 'Creates a new spec'
39
- option :path,
40
- type: :string, required: false, desc: 'The path where your spec will be created', aliases: '-p'
41
- option :delete,
42
- type: :boolean, required: false, desc: 'This will delete the selected spec', aliases: '-d'
46
+ option :path, type: :string, required: false,
47
+ desc: 'The path where your spec will be created', aliases: '-p'
48
+ option :delete, type: :boolean, required: false,
49
+ desc: 'This will delete the selected spec', aliases: '-d'
50
+ option :from, type: :string, required: false,
51
+ desc: 'Generate spec stubs from an existing page object file', aliases: '-f'
52
+ option :uses, type: :array, required: false,
53
+ desc: 'Dependent pages to require', aliases: '-u'
54
+ option :ai, type: :boolean, default: false,
55
+ desc: 'Use LLM to generate meaningful test scenarios'
43
56
 
44
57
  def spec(name)
45
58
  return delete_scaffolding(name, 'spec') if options[:delete]
59
+ return dry_run_preview(name, 'spec') if options[:dry_run]
60
+ return generate_spec_from_page(name, options[:from], ai: options[:ai]) if options[:from]
46
61
 
47
- generate_scaffolding(name, 'spec', options[:path])
62
+ generate_scaffolding(name, 'spec', options[:path], uses: options[:uses])
48
63
  end
49
64
 
50
65
  desc 'helper [HELPER_NAME]', 'Creates a new helper'
51
- option :path,
52
- type: :string, required: false, desc: 'The path where your helper will be created', aliases: '-p'
53
- option :delete,
54
- type: :boolean, required: false, desc: 'This will delete the selected helper', aliases: '-d'
66
+ option :path, type: :string, required: false,
67
+ desc: 'The path where your helper will be created', aliases: '-p'
68
+ option :delete, type: :boolean, required: false,
69
+ desc: 'This will delete the selected helper', aliases: '-d'
55
70
 
56
71
  def helper(name)
57
72
  return delete_scaffolding(name, 'helper') if options[:delete]
73
+ return dry_run_preview(name, 'helper') if options[:dry_run]
58
74
 
59
75
  generate_scaffolding(name, 'helper', options[:path])
60
76
  end
61
77
 
62
78
  desc 'steps [STEPS_NAME]', 'Creates a new steps definition'
63
- option :path,
64
- type: :string, required: false, desc: 'The path where your steps will be created', aliases: '-p'
65
- option :delete,
66
- type: :boolean, required: false, desc: 'This will delete the selected steps', aliases: '-d'
79
+ option :path, type: :string, required: false,
80
+ desc: 'The path where your steps will be created', aliases: '-p'
81
+ option :delete, type: :boolean, required: false,
82
+ desc: 'This will delete the selected steps', aliases: '-d'
67
83
 
68
84
  def steps(name)
69
85
  return delete_scaffolding(name, 'steps') if options[:delete]
86
+ return dry_run_preview(name, 'steps') if options[:dry_run]
70
87
 
71
88
  generate_scaffolding(name, 'steps', options[:path])
72
89
  end
73
90
 
74
- desc 'scaffold [SCAFFOLD_NAME]', 'It generates everything needed to start automating'
91
+ desc 'component [NAME]', 'Creates a component inheriting from Component'
92
+ option :path, type: :string, required: false,
93
+ desc: 'The path where your component will be created', aliases: '-p'
94
+ option :delete, type: :boolean, required: false,
95
+ desc: 'This will delete the selected component', aliases: '-d'
96
+
97
+ def component(name)
98
+ return delete_scaffolding(name, 'component') if options[:delete]
99
+ return dry_run_preview(name, 'component') if options[:dry_run]
100
+
101
+ generate_scaffolding(name, 'component', options[:path])
102
+ end
103
+
104
+ desc 'scaffold [NAMES...]', 'Generates pages, specs/features, and helpers for one or more names'
105
+ option :with, type: :array, required: false,
106
+ desc: 'Components to generate (page,spec,feature,steps,helper,component,model)', aliases: '-w'
107
+ option :crud, type: :boolean, required: false,
108
+ desc: 'Generate CRUD pages (list, create, detail, edit) + tests + model'
109
+ option :uses, type: :array, required: false,
110
+ desc: 'Dependent pages to require', aliases: '-u'
111
+
112
+ def scaffold(*names)
113
+ return interactive_scaffold if names.empty?
114
+
115
+ names.each do |name|
116
+ if options[:crud]
117
+ generate_crud(name)
118
+ elsif options[:with]
119
+ generate_selected_components(name, options[:with])
120
+ else
121
+ generate_default_scaffold(name)
122
+ end
123
+ end
124
+ end
125
+
126
+ desc 'destroy [NAMES...]', 'Removes all scaffolded files for the given names (page, spec/feature, steps)'
127
+ option :with, type: :array, required: false,
128
+ desc: 'Components to destroy (page,spec,feature,steps,helper,component)', aliases: '-w'
129
+
130
+ def destroy(*names)
131
+ if names.empty?
132
+ say 'Please provide at least one name to destroy', :red
133
+ return
134
+ end
135
+
136
+ names.each { |name| destroy_scaffold(name, options[:with]) }
137
+ end
138
+
139
+ map 'd' => 'destroy'
140
+
141
+ desc 'from_url [URL]', 'Generates page object and spec from a live URL'
142
+ option :name, type: :string, required: false,
143
+ desc: 'Override the page object name', aliases: '-n'
144
+ option :ai, type: :boolean, default: false,
145
+ desc: 'Use LLM for intelligent page analysis'
146
+
147
+ def from_url(url)
148
+ require_relative '../scaffolding/url_analyzer'
149
+ analyzer = UrlAnalyzer.new(url, name_override: options[:name], ai: options[:ai])
150
+ analysis = analyzer.analyze.to_h
151
+
152
+ page_name = analysis[:page_name]
75
153
 
76
- def scaffold(name)
77
- if Pathname.new('spec').exist? && !Pathname.new('features').exist?
78
- Scaffolding.new([name, load_config_path('spec')]).generate_spec
79
- else
80
- Scaffolding.new([name, load_config_path('feature')]).generate_feature
81
- Scaffolding.new([name, load_config_path('steps')]).generate_steps
154
+ if options[:dry_run]
155
+ DryRunPresenter.preview([
156
+ "page_objects/pages/#{page_name}.rb",
157
+ "spec/#{page_name}_spec.rb"
158
+ ])
159
+ return
82
160
  end
83
- Scaffolding.new([name, load_config_path('page')]).generate_page
161
+
162
+ Scaffolding.new([page_name]).generate_page_from_url(analysis)
163
+ Scaffolding.new([page_name]).generate_spec_from_url(analysis)
164
+ say "Generated page object and spec for #{url}"
84
165
  end
85
166
 
86
167
  no_commands do
87
168
  def load_config_path(type)
88
- YAML.load_file('config/config.yml')["#{type}_path"] if Pathname.new('config/config.yml').exist?
169
+ ScaffoldProjectDetector.config_path(type)
170
+ end
171
+
172
+ def validate_project!
173
+ warnings = ScaffoldProjectDetector.validate_project
174
+ warnings.each { |w| say "Warning: #{w}", :yellow }
175
+ show_detection_defaults
176
+ end
177
+
178
+ def show_detection_defaults
179
+ gemfile = ScaffoldProjectDetector.read_gemfile
180
+ if gemfile.empty?
181
+ say 'Warning: No Gemfile found, defaulting to selenium + rspec templates.', :yellow
182
+ else
183
+ automation = ScaffoldProjectDetector.detect_automation(gemfile)
184
+ framework = ScaffoldProjectDetector.detect_framework(gemfile)
185
+ if automation.nil?
186
+ say 'Warning: Could not detect automation library from Gemfile, defaulting to selenium.', :yellow
187
+ end
188
+ say 'Warning: Could not detect test framework from Gemfile, defaulting to rspec.', :yellow if framework.nil?
189
+ end
89
190
  end
90
191
 
91
192
  def delete_scaffolding(name, type)
92
193
  Scaffolding.new([name]).send("delete_#{type}")
93
194
  end
94
195
 
95
- def generate_scaffolding(name, type, path)
196
+ def generate_scaffolding(name, type, path, uses: nil)
96
197
  path ||= load_config_path(type)
97
- Scaffolding.new([name, path]).send("generate_#{type}")
198
+ scaffolding = Scaffolding.new([name, path])
199
+ scaffolding.uses = Array(uses) if uses
200
+ scaffolding.send("generate_#{type}")
201
+ end
202
+
203
+ def dry_run_preview(name, type)
204
+ path = options[:path] || load_config_path(type)
205
+ file = Scaffolding.planned_path(name, type, path)
206
+ DryRunPresenter.preview([file])
207
+ end
208
+
209
+ def generate_default_scaffold(name)
210
+ validate_project!
211
+ uses = options[:uses]
212
+ if Pathname.new('spec').exist? && !Pathname.new('features').exist?
213
+ generate_scaffolding(name, 'spec', load_config_path('spec'), uses:)
214
+ else
215
+ generate_scaffolding(name, 'feature', load_config_path('feature'))
216
+ generate_scaffolding(name, 'steps', load_config_path('steps'), uses:)
217
+ end
218
+ generate_scaffolding(name, 'page', load_config_path('page'), uses:)
219
+ end
220
+
221
+ def generate_selected_components(name, components)
222
+ validate_project!
223
+ uses = options[:uses]
224
+ components.each do |comp|
225
+ comp = comp.downcase.strip
226
+ case comp
227
+ when 'model'
228
+ generate_model_data(name)
229
+ else
230
+ generate_scaffolding(name, comp, load_config_path(comp), uses:)
231
+ end
232
+ end
233
+ end
234
+
235
+ def generate_crud(name)
236
+ require_relative '../scaffolding/crud_generator'
237
+ validate_project!
238
+ if options[:dry_run]
239
+ crud = CrudGenerator.new(name, Scaffolding, method(:load_config_path))
240
+ DryRunPresenter.preview(crud.planned_files)
241
+ return
242
+ end
243
+ crud = CrudGenerator.new(name, Scaffolding, method(:load_config_path))
244
+ generated = crud.generate
245
+ say "Generated CRUD scaffold for: #{generated.join(', ')}"
246
+ end
247
+
248
+ def generate_spec_from_page(name, source_file, ai: false) # rubocop:disable Naming/MethodParameterName
249
+ Scaffolding.new([name]).generate_spec_from_page(source_file, ai:)
250
+ end
251
+
252
+ def generate_model_data(name)
253
+ normalized = NameNormalizer.normalize(name)
254
+ path = "models/data/#{normalized}.yml"
255
+ return if File.exist?(path)
256
+
257
+ FileUtils.mkdir_p(File.dirname(path))
258
+ File.write(path, model_template(normalized))
259
+ end
260
+
261
+ def model_template(name)
262
+ <<~YAML
263
+ # Data model for #{name}
264
+ default:
265
+ name: 'Test #{name.capitalize}'
266
+ email: 'test@example.com'
267
+
268
+ valid:
269
+ name: 'Valid #{name.capitalize}'
270
+ email: 'valid@example.com'
271
+
272
+ invalid:
273
+ name: ''
274
+ email: 'invalid'
275
+ YAML
276
+ end
277
+
278
+ def destroy_scaffold(name, components = nil)
279
+ types = components ? components.map { |c| c.downcase.strip } : detect_scaffold_types
280
+ types.each { |type| delete_scaffolding(name, type) }
281
+ end
282
+
283
+ def detect_scaffold_types
284
+ if Pathname.new('spec').exist? && !Pathname.new('features').exist?
285
+ %w[page spec]
286
+ else
287
+ %w[page feature steps]
288
+ end
289
+ end
290
+
291
+ def interactive_scaffold
292
+ require_relative '../scaffolding/scaffold_menu'
293
+ validate_project!
294
+ result = ScaffoldMenu.new.run
295
+ return unless result
296
+
297
+ result[:names].each do |name|
298
+ result[:components].each do |comp|
299
+ if comp == :model
300
+ generate_model_data(name)
301
+ else
302
+ generate_scaffolding(name, comp.to_s, load_config_path(comp.to_s), uses: result[:uses])
303
+ end
304
+ end
305
+ end
98
306
  end
99
307
  end
100
308
  end
@@ -3,8 +3,6 @@
3
3
  require 'thor'
4
4
  require_relative '../utilities/utilities'
5
5
 
6
- # :reek:FeatureEnvy { enabled: false }
7
- # :reek:UtilityFunction { enabled: false }
8
6
  class UtilityCommands < Thor
9
7
  desc 'path [PATH]', 'Sets the default path for scaffolding'
10
8
  option :feature,
@@ -37,7 +35,7 @@ class UtilityCommands < Thor
37
35
  browser_options(selected_options) if selected_options || options[:delete]
38
36
  end
39
37
 
40
- desc 'browser_options [OPTIONS]', 'Sets the browser options for the project'
38
+ desc 'browser_options [OPTIONS]', 'Sets the browser arguments for the current browser (e.g. no-sandbox, headless)'
41
39
  option :delete,
42
40
  type: :boolean, required: false, desc: 'This will delete your browser options', aliases: '-d'
43
41
 
@@ -46,6 +44,15 @@ class UtilityCommands < Thor
46
44
  Utilities.delete_browser_options if options[:delete]
47
45
  end
48
46
 
47
+ desc 'headless [on/off]', 'Toggles headless mode for browser tests'
48
+
49
+ def headless(toggle)
50
+ enabled = %w[on true 1 yes].include?(toggle.downcase)
51
+ Utilities.headless = enabled
52
+ state = enabled ? 'enabled' : 'disabled'
53
+ say "Headless mode #{state}", :green
54
+ end
55
+
49
56
  desc 'raid', 'It runs all the tests in a project'
50
57
  option :parallel,
51
58
  type: :boolean, required: false, desc: 'It runs the tests in parallel', aliases: '-p'
@@ -67,14 +74,96 @@ class UtilityCommands < Thor
67
74
  option :delete,
68
75
  type: :boolean, required: false, desc: 'This will delete the selected config file', aliases: '-d'
69
76
 
77
+ desc 'timeout [SECONDS]', 'Sets the default test timeout in seconds'
78
+
79
+ def timeout(seconds)
80
+ Utilities.timeout = seconds
81
+ end
82
+
83
+ desc 'viewport [DIMENSIONS]', 'Sets the default viewport size (e.g. 1920x1080, 375x812)'
84
+
85
+ def viewport(dimensions)
86
+ Utilities.viewport = dimensions
87
+ end
88
+
70
89
  desc 'platform [PLATFORM]', 'Sets the default platform for a cross-platform project'
71
90
 
72
91
  def platform(platform)
73
92
  Utilities.platform = platform
74
93
  end
75
94
 
95
+ desc 'debug [on/off]', 'Toggles debug mode for failure diagnostics and logging'
96
+
97
+ def debug(toggle)
98
+ enabled = %w[on true 1 yes].include?(toggle.downcase)
99
+ Utilities.debug = enabled
100
+ state = enabled ? 'enabled' : 'disabled'
101
+ say "Debug mode #{state}", :green
102
+ end
103
+
76
104
  desc 'start_appium', 'It starts the appium server'
77
105
  def start_appium
78
106
  system 'appium --base-path /wd/hub'
79
107
  end
108
+
109
+ desc 'desktop', 'Downloads the Raider Desktop GUI application'
110
+ option :path, type: :string, required: false,
111
+ desc: 'Directory to save the download', aliases: '-p'
112
+
113
+ def desktop
114
+ require_relative '../utilities/desktop_downloader'
115
+ version = DesktopDownloader.latest_version
116
+ unless version
117
+ say 'Could not reach GitHub releases. Check your internet connection.', :red
118
+ return
119
+ end
120
+
121
+ say "Raider Desktop v#{version} available for #{DesktopDownloader.platform_display_name}"
122
+ DesktopDownloader.download(options[:path])
123
+ say 'Raider Desktop downloaded successfully!', :green
124
+ end
125
+
126
+ desc 'llm [PROVIDER]', 'Configures the LLM provider (openai, anthropic, ollama)'
127
+ option :key, type: :string, required: false, desc: 'API key for the provider', aliases: '-k'
128
+ option :model, type: :string, required: false, desc: 'Model name to use', aliases: '-m'
129
+ option :url, type: :string, required: false, desc: 'API URL (for ollama)', aliases: '-u'
130
+ option :status, type: :boolean, required: false, desc: 'Show current LLM configuration', aliases: '-s'
131
+
132
+ def llm(provider = nil)
133
+ if options[:status] || provider.nil?
134
+ show_llm_status
135
+ return
136
+ end
137
+ configure_llm(provider)
138
+ end
139
+
140
+ no_commands do
141
+ def configure_llm(provider)
142
+ unless %w[openai anthropic ollama].include?(provider)
143
+ say "Unknown provider '#{provider}'. Choose: openai, anthropic, ollama", :red
144
+ return
145
+ end
146
+
147
+ Utilities.llm_provider = provider
148
+ Utilities.llm_api_key = options[:key] if options[:key]
149
+ Utilities.llm_model = options[:model] if options[:model]
150
+ Utilities.llm_url = options[:url] if options[:url]
151
+ say "LLM configured: #{provider}", :green
152
+ end
153
+
154
+ def show_llm_status
155
+ require_relative '../llm/client'
156
+ status = Llm::Client.status
157
+ if status[:configured]
158
+ say "Provider: #{status[:provider]}"
159
+ say "Model: #{status[:model] || 'default'}"
160
+ say "Available: #{status[:available] ? 'yes' : 'no'}"
161
+ else
162
+ say 'No LLM configured. Use: raider u llm <provider>', :yellow
163
+ say ' Providers: openai, anthropic, ollama'
164
+ say ' Example: raider u llm ollama'
165
+ say ' Example: raider u llm openai -k sk-...'
166
+ end
167
+ end
168
+ end
80
169
  end
@@ -9,7 +9,6 @@ class AutomationGenerator < Generator
9
9
  generate_home_page
10
10
  generate_pdp_page
11
11
  else
12
- generate_visual_options
13
12
  generate_components
14
13
  generate_model_files
15
14
  generate_pages
@@ -47,12 +46,6 @@ class AutomationGenerator < Generator
47
46
  template('appium_caps.tt', "#{name}/config/capabilities.yml")
48
47
  end
49
48
 
50
- def generate_visual_options
51
- return unless visual?
52
-
53
- template('visual_options.tt', "#{name}/config/options.yml")
54
- end
55
-
56
49
  def generate_login_page
57
50
  template('login.tt', "#{name}/page_objects/pages/login.rb")
58
51
  end
@@ -1,4 +1,4 @@
1
- <% if %w[selenium axe applitools appium_ios appium_android].include?(automation) -%>
1
+ <% if %w[selenium appium_ios appium_android].include?(automation) -%>
2
2
  driver.find_element
3
3
  <% else -%>
4
4
  browser.element
@@ -6,13 +6,6 @@
6
6
  def initialize(browser)
7
7
  @browser = browser
8
8
  end
9
- <% elsif axe? %>
10
- attr_reader :driver
11
- alias page driver
12
-
13
- def initialize(driver)
14
- @driver = driver
15
- end
16
9
  <% else %>
17
10
  attr_reader :driver
18
11
 
@@ -3,7 +3,6 @@
3
3
  "#{base_url}#{url(*page)}"
4
4
  end
5
5
 
6
- # :reek:UtilityFunction
7
6
  def base_url
8
7
  YAML.load_file('config/config.yml')['url']
9
8
  end