ruby_raider 2.0.0 → 3.0.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 (224) 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 +220 -37
  20. data/lib/commands/utility_commands.rb +82 -2
  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 +12 -2
  71. data/lib/generators/templates/helpers/capybara_helper.tt +5 -1
  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 +8 -7
  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 +72 -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 +53 -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/generators/generator_spec.rb +23 -0
  178. data/spec/integration/commands/scaffolding_commands_spec.rb +1 -1
  179. data/spec/integration/commands/utility_commands_spec.rb +23 -3
  180. data/spec/integration/content/ci_content_spec.rb +119 -0
  181. data/spec/integration/content/common_content_spec.rb +288 -0
  182. data/spec/integration/content/config_content_spec.rb +175 -0
  183. data/spec/integration/content/content_helper.rb +32 -0
  184. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  185. data/spec/integration/content/helper_content_spec.rb +485 -0
  186. data/spec/integration/content/page_content_spec.rb +259 -0
  187. data/spec/integration/content/reporter_content_spec.rb +236 -0
  188. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  189. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  190. data/spec/integration/content/test_content_spec.rb +266 -0
  191. data/spec/integration/end_to_end_features_spec.rb +690 -0
  192. data/spec/integration/end_to_end_spec.rb +52 -16
  193. data/spec/integration/generators/automation_generator_spec.rb +0 -12
  194. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  195. data/spec/integration/generators/common_generator_spec.rb +12 -13
  196. data/spec/integration/generators/config_features_spec.rb +155 -0
  197. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  198. data/spec/integration/generators/helpers_generator_spec.rb +0 -12
  199. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  200. data/spec/integration/generators/minitest_generator_spec.rb +0 -6
  201. data/spec/integration/generators/reporter_spec.rb +159 -0
  202. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  203. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  204. data/spec/integration/settings_helper.rb +0 -3
  205. data/spec/integration/spec_helper.rb +30 -13
  206. data/spec/llm/client_spec.rb +79 -0
  207. data/spec/llm/config_spec.rb +92 -0
  208. data/spec/llm/prompts_spec.rb +49 -0
  209. data/spec/llm/response_parser_spec.rb +92 -0
  210. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  211. data/spec/menus/menu_generator_spec.rb +263 -0
  212. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  213. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  214. data/spec/scaffolding/scaffold_project_detector_spec.rb +104 -0
  215. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  216. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  217. data/spec/system/adopt_matrix_spec.rb +537 -0
  218. data/spec/system/adopt_spec.rb +225 -0
  219. data/spec/system/support/system_test_helper.rb +0 -2
  220. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  221. metadata +150 -5
  222. data/lib/generators/automation/templates/visual_options.tt +0 -16
  223. data/lib/generators/templates/helpers/partials/axe_driver.tt +0 -10
  224. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
data/lib/ruby_raider.rb CHANGED
@@ -1,21 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../lib/plugin/plugin'
4
- require_relative '../lib/commands/plugin_commands'
5
- require_relative '../lib/commands/loaded_commands'
6
- require_relative '../lib/commands/adopt_commands'
7
- require_relative '../lib/commands/scaffolding_commands'
8
- require_relative '../lib/commands/utility_commands'
9
-
10
- # :reek:FeatureEnvy { enabled: false }
11
- # :reek:UtilityFunction { enabled: false }
3
+ require 'thor'
4
+
5
+ # Lazy-load top-level command classes to reduce CLI startup time.
6
+ autoload :AdoptCommands, File.expand_path('commands/adopt_commands', __dir__)
7
+ autoload :ScaffoldingCommands, File.expand_path('commands/scaffolding_commands', __dir__)
8
+ autoload :UtilityCommands, File.expand_path('commands/utility_commands', __dir__)
9
+
12
10
  module RubyRaider
11
+ # Lazy-load namespaced classes within the correct module scope.
12
+ autoload :Plugin, File.expand_path('plugin/plugin', __dir__)
13
+ autoload :PluginCommands, File.expand_path('commands/plugin_commands', __dir__)
14
+ autoload :LoadedCommands, File.expand_path('commands/loaded_commands', __dir__)
15
+
13
16
  class Raider < Thor
14
17
  no_tasks do
15
18
  def self.plugin_commands?
16
- File.readlines(File.expand_path('commands/loaded_commands.rb', __dir__)).any? do |line|
17
- line.include?('subcommand')
18
- end
19
+ @plugin_commands ||= File.read(
20
+ File.expand_path('commands/loaded_commands.rb', __dir__)
21
+ ).include?('subcommand')
19
22
  end
20
23
 
21
24
  def current_version = File.read(File.expand_path('version', __dir__)).strip
@@ -24,12 +27,31 @@ module RubyRaider
24
27
  desc 'new [PROJECT_NAME]', 'Creates a new framework based on settings picked'
25
28
  option :parameters,
26
29
  type: :hash, required: false, desc: 'Parameters to avoid using the menu', aliases: 'p'
30
+ option :skip_ci,
31
+ type: :boolean, required: false, desc: 'Skip CI/CD configuration generation'
32
+ option :skip_allure,
33
+ type: :boolean, required: false, desc: 'Skip Allure reporting setup'
34
+ option :skip_video,
35
+ type: :boolean, required: false, desc: 'Skip video recording setup'
36
+ option :reporter,
37
+ type: :string, required: false, desc: 'Reporter: allure, junit, json, both, all, none', aliases: '-r'
38
+ option :accessibility,
39
+ type: :boolean, required: false, desc: 'Add axe accessibility testing'
40
+ option :visual,
41
+ type: :boolean, required: false, desc: 'Add visual regression testing'
42
+ option :performance,
43
+ type: :boolean, required: false, desc: 'Add Lighthouse performance auditing'
44
+ option :ruby_version,
45
+ type: :string, required: false, desc: 'Ruby version for generated project (e.g. 3.4, 3.3)'
27
46
 
28
47
  def new(project_name)
48
+ require_relative 'utilities/logo'
49
+ RubyRaider::Logo.display
29
50
  params = options[:parameters]
30
51
  if params
31
52
  params[:name] = project_name
32
53
  parsed_hash = params.transform_keys(&:to_sym)
54
+ merge_skip_flags(parsed_hash)
33
55
  return InvokeGenerators.generate_framework(parsed_hash)
34
56
  end
35
57
 
@@ -67,5 +89,18 @@ module RubyRaider
67
89
  subcommand 'plugins', LoadedCommands
68
90
  map 'ps' => 'plugins'
69
91
  end
92
+
93
+ no_tasks do
94
+ def merge_skip_flags(params)
95
+ %i[skip_ci skip_allure skip_video].each do |flag|
96
+ params[flag] = true if options[flag]
97
+ end
98
+ params[:reporter] = options[:reporter] if options[:reporter]
99
+ params[:accessibility] = true if options[:accessibility]
100
+ params[:visual] = true if options[:visual]
101
+ params[:performance] = true if options[:performance]
102
+ params[:ruby_version] = options[:ruby_version] if options[:ruby_version]
103
+ end
104
+ end
70
105
  end
71
106
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'name_normalizer'
4
+
5
+ class CrudGenerator
6
+ ACTIONS = %w[list create detail edit].freeze
7
+
8
+ def initialize(base_name, scaffolding_class, config_loader)
9
+ @base_name = NameNormalizer.normalize(base_name)
10
+ @scaffolding_class = scaffolding_class
11
+ @config_loader = config_loader
12
+ end
13
+
14
+ def generate
15
+ generated = []
16
+ ACTIONS.each do |action|
17
+ name = "#{@base_name}_#{action}"
18
+ generate_page(name)
19
+ generate_test(name)
20
+ generated << name
21
+ end
22
+ generate_model
23
+ generated
24
+ end
25
+
26
+ def planned_files
27
+ files = []
28
+ ACTIONS.each do |action|
29
+ name = "#{@base_name}_#{action}"
30
+ files << "page_objects/pages/#{name}.rb"
31
+ files << test_path(name)
32
+ end
33
+ files << "models/data/#{@base_name}.yml"
34
+ files
35
+ end
36
+
37
+ private
38
+
39
+ def generate_page(name)
40
+ path = @config_loader.call('page')
41
+ @scaffolding_class.new([name, path]).generate_page
42
+ end
43
+
44
+ def generate_test(name)
45
+ if Dir.exist?('features')
46
+ path = @config_loader.call('feature')
47
+ @scaffolding_class.new([name, path]).generate_feature
48
+ path = @config_loader.call('steps')
49
+ @scaffolding_class.new([name, path]).generate_steps
50
+ elsif Dir.exist?('test')
51
+ path = @config_loader.call('spec')
52
+ @scaffolding_class.new([name, path]).generate_spec
53
+ else
54
+ path = @config_loader.call('spec')
55
+ @scaffolding_class.new([name, path]).generate_spec
56
+ end
57
+ end
58
+
59
+ def generate_model
60
+ model_path = "models/data/#{@base_name}.yml"
61
+ return if File.exist?(model_path)
62
+
63
+ FileUtils.mkdir_p(File.dirname(model_path))
64
+ File.write(model_path, model_content)
65
+ end
66
+
67
+ def model_content
68
+ <<~YAML
69
+ # Data model for #{@base_name}
70
+ # Used with ModelFactory for test data generation
71
+ default:
72
+ name: 'Test #{@base_name.capitalize}'
73
+ email: 'test@example.com'
74
+
75
+ valid:
76
+ name: 'Valid #{@base_name.capitalize}'
77
+ email: 'valid@example.com'
78
+
79
+ invalid:
80
+ name: ''
81
+ email: 'invalid'
82
+ YAML
83
+ end
84
+
85
+ def test_path(name)
86
+ if Dir.exist?('features')
87
+ "features/#{name}.feature"
88
+ elsif Dir.exist?('test')
89
+ "test/test_#{name}.rb"
90
+ else
91
+ "spec/#{name}_page_spec.rb"
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DryRunPresenter
4
+ module_function
5
+
6
+ def preview(planned_files)
7
+ return [] if planned_files.empty?
8
+
9
+ puts '[dry-run] Would create:'
10
+ planned_files.each do |file|
11
+ puts " #{file}"
12
+ end
13
+ puts ''
14
+ planned_files
15
+ end
16
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NameNormalizer
4
+ SUFFIXES = %w[_page _spec _steps _helper _test _feature].freeze
5
+
6
+ module_function
7
+
8
+ # Normalize raw user input to a clean base name
9
+ # 'LoginPage' -> 'login', 'login_page' -> 'login', 'admin/users' -> 'admin/users'
10
+ def normalize(input)
11
+ name = input.to_s.strip
12
+ name = camel_to_snake(name) if name.match?(/[A-Z]/)
13
+ strip_suffixes(name)
14
+ end
15
+
16
+ # Convert to class name: 'login' -> 'LoginPage', 'admin/users' -> 'Admin::UsersPage'
17
+ def to_class_name(input, suffix = '')
18
+ normalized = normalize(input)
19
+ parts = normalized.split('/')
20
+ parts.map { |part| part.split('_').map(&:capitalize).join }.join('::') + suffix
21
+ end
22
+
23
+ # Convert to page class: 'login' -> 'LoginPage'
24
+ def to_page_class(input)
25
+ to_class_name(input, 'Page')
26
+ end
27
+
28
+ # Convert to file name: 'LoginPage' -> 'login', 'admin/users' -> 'admin/users'
29
+ def to_file_name(input)
30
+ normalize(input)
31
+ end
32
+
33
+ # Check if the input contains a nested path
34
+ def nested?(input)
35
+ normalize(input).include?('/')
36
+ end
37
+
38
+ # Get the module path for nested names: 'admin/users' -> ['Admin']
39
+ def module_parts(input)
40
+ parts = normalize(input).split('/')
41
+ parts[0..-2].map { |p| p.split('_').map(&:capitalize).join }
42
+ end
43
+
44
+ # Get the leaf name: 'admin/users' -> 'users'
45
+ def leaf_name(input)
46
+ normalize(input).split('/').last
47
+ end
48
+
49
+ def camel_to_snake(str)
50
+ str.gsub(/::/, '/')
51
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
52
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
53
+ .downcase
54
+ end
55
+
56
+ def strip_suffixes(name)
57
+ result = name.dup
58
+ SUFFIXES.each do |suffix|
59
+ result = result.delete_suffix(suffix) if result.end_with?(suffix)
60
+ end
61
+ result
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
4
+
5
+ class PageIntrospector
6
+ SKIP_METHODS = %w[initialize url to_s inspect].freeze
7
+
8
+ attr_reader :class_name, :methods
9
+
10
+ def initialize(file_path)
11
+ @source = File.read(file_path)
12
+ @class_name = extract_class_name
13
+ @methods = extract_public_methods
14
+ end
15
+
16
+ private
17
+
18
+ def extract_class_name
19
+ match = @source.match(/class\s+(\w+)/)
20
+ match ? match[1] : 'UnknownPage'
21
+ end
22
+
23
+ def extract_public_methods
24
+ in_private = false
25
+ results = []
26
+
27
+ @source.each_line do |line|
28
+ stripped = line.strip
29
+ in_private = true if stripped.match?(/^\s*(private|protected)\s*$/)
30
+ next if in_private
31
+
32
+ match = stripped.match(/^\s*def\s+(\w+)(\(([^)]*)\))?/)
33
+ next unless match
34
+
35
+ method_name = match[1]
36
+ next if SKIP_METHODS.include?(method_name)
37
+ next if method_name.start_with?('_')
38
+
39
+ params = match[3]&.split(',')&.map(&:strip) || []
40
+ results << { name: method_name, params: }
41
+ end
42
+
43
+ results
44
+ end
45
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScaffoldProjectDetector
4
+ AUTOMATION_GEMS = {
5
+ 'capybara' => 'capybara',
6
+ 'watir' => 'watir',
7
+ 'selenium-webdriver' => 'selenium',
8
+ 'appium_lib' => 'appium',
9
+ 'eyes_selenium' => 'selenium',
10
+ 'axe-core-rspec' => 'selenium',
11
+ 'axe-core-cucumber' => 'selenium'
12
+ }.freeze
13
+
14
+ FRAMEWORK_GEMS = {
15
+ 'rspec' => 'rspec',
16
+ 'cucumber' => 'cucumber',
17
+ 'minitest' => 'minitest'
18
+ }.freeze
19
+
20
+ module_function
21
+
22
+ def detect
23
+ gemfile = read_gemfile
24
+ {
25
+ automation: detect_automation(gemfile),
26
+ framework: detect_framework(gemfile),
27
+ has_spec: Dir.exist?('spec'),
28
+ has_features: Dir.exist?('features'),
29
+ has_test: Dir.exist?('test')
30
+ }
31
+ end
32
+
33
+ def detect_automation(gemfile = read_gemfile)
34
+ AUTOMATION_GEMS.each do |gem_name, automation|
35
+ return automation if gemfile.include?("'#{gem_name}'") || gemfile.include?("\"#{gem_name}\"")
36
+ end
37
+ 'selenium'
38
+ end
39
+
40
+ def detect_framework(gemfile = read_gemfile)
41
+ FRAMEWORK_GEMS.each do |gem_name, framework|
42
+ return framework if gemfile.include?("'#{gem_name}'") || gemfile.include?("\"#{gem_name}\"")
43
+ end
44
+ 'rspec'
45
+ end
46
+
47
+ def read_gemfile
48
+ return '' unless File.exist?('Gemfile')
49
+
50
+ File.read('Gemfile')
51
+ end
52
+
53
+ def selenium?
54
+ detect_automation == 'selenium'
55
+ end
56
+
57
+ def capybara?
58
+ detect_automation == 'capybara'
59
+ end
60
+
61
+ def watir?
62
+ detect_automation == 'watir'
63
+ end
64
+
65
+ def config
66
+ return {} unless File.exist?('config/config.yml')
67
+
68
+ YAML.safe_load(File.read('config/config.yml'), permitted_classes: [Symbol]) || {}
69
+ rescue StandardError
70
+ {}
71
+ end
72
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+ require_relative 'project_detector'
5
+
6
+ class ScaffoldMenu
7
+ COMPONENTS = {
8
+ 'Page object' => :page,
9
+ 'Spec (RSpec)' => :spec,
10
+ 'Feature (Cucumber)' => :feature,
11
+ 'Steps (Cucumber)' => :steps,
12
+ 'Helper' => :helper,
13
+ 'Component' => :component,
14
+ 'Model data' => :model
15
+ }.freeze
16
+
17
+ def initialize
18
+ @prompt = TTY::Prompt.new
19
+ end
20
+
21
+ def run
22
+ names = ask_names
23
+ components = ask_components
24
+ uses = ask_relationships
25
+ preview_and_confirm(names, components, uses)
26
+ end
27
+
28
+ # Programmatic entry point for raider_desktop
29
+ def self.build_options(names:, components:, uses: [])
30
+ { names: Array(names), components: Array(components), uses: Array(uses) }
31
+ end
32
+
33
+ private
34
+
35
+ def ask_names
36
+ input = @prompt.ask('Enter scaffold name(s), comma-separated:') do |q|
37
+ q.required true
38
+ end
39
+ input.split(',').map(&:strip).reject(&:empty?)
40
+ end
41
+
42
+ def ask_components
43
+ project = ScaffoldProjectDetector.detect
44
+ defaults = default_components(project)
45
+
46
+ @prompt.multi_select('Select components to generate:', default: defaults) do |menu|
47
+ COMPONENTS.each do |label, value|
48
+ menu.choice label, value
49
+ end
50
+ end
51
+ end
52
+
53
+ def ask_relationships
54
+ return [] unless @prompt.yes?('Add page dependencies (--uses)?', default: false)
55
+
56
+ pages = Dir.glob('page_objects/pages/*.rb').map { |f| File.basename(f, '.rb') }
57
+ return [] if pages.empty?
58
+
59
+ @prompt.multi_select('Select dependent pages:', pages)
60
+ end
61
+
62
+ def preview_and_confirm(names, components, uses)
63
+ files = planned_files(names, components)
64
+
65
+ @prompt.say("\nWill create:")
66
+ files.each { |f| @prompt.say(" #{f}") }
67
+ @prompt.say('')
68
+
69
+ return nil unless @prompt.yes?('Proceed?')
70
+
71
+ { names:, components:, uses: }
72
+ end
73
+
74
+ def planned_files(names, components)
75
+ names.flat_map do |name|
76
+ components.map { |comp| file_for(name, comp) }
77
+ end
78
+ end
79
+
80
+ def file_for(name, component)
81
+ case component
82
+ when :page then "page_objects/pages/#{name}.rb"
83
+ when :spec then "spec/#{name}_page_spec.rb"
84
+ when :feature then "features/#{name}.feature"
85
+ when :steps then "features/step_definitions/#{name}_steps.rb"
86
+ when :helper then "helpers/#{name}_helper.rb"
87
+ when :component then "page_objects/components/#{name}.rb"
88
+ when :model then "models/data/#{name}.yml"
89
+ end
90
+ end
91
+
92
+ def default_components(project)
93
+ defaults = [:page]
94
+ if project[:has_features]
95
+ defaults += %i[feature steps]
96
+ elsif project[:has_test]
97
+ defaults << :spec
98
+ else
99
+ defaults << :spec
100
+ end
101
+ defaults.map { |d| COMPONENTS.key(d) }.compact
102
+ end
103
+ end