ruby_raider 1.1.4 → 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 (278) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +58 -0
  3. data/.github/workflows/integration.yml +4 -6
  4. data/.github/workflows/reek.yml +6 -5
  5. data/.github/workflows/release.yml +175 -0
  6. data/.github/workflows/rubocop.yml +7 -6
  7. data/.github/workflows/steep.yml +21 -0
  8. data/.github/workflows/system_tests.yml +83 -0
  9. data/.gitignore +1 -1
  10. data/.reek.yml +46 -4
  11. data/.rubocop.yml +24 -0
  12. data/.ruby-version +1 -1
  13. data/README.md +140 -77
  14. data/RELEASE.md +412 -0
  15. data/RELEASE_QUICK_GUIDE.md +77 -0
  16. data/Steepfile +22 -0
  17. data/assets/ruby_raider_logo.svg +51 -0
  18. data/bin/release +186 -0
  19. data/lib/adopter/adopt_menu.rb +146 -0
  20. data/lib/adopter/converters/base_converter.rb +84 -0
  21. data/lib/adopter/converters/identity_converter.rb +53 -0
  22. data/lib/adopter/migration_plan.rb +74 -0
  23. data/lib/adopter/migrator.rb +96 -0
  24. data/lib/adopter/plan_builder.rb +275 -0
  25. data/lib/adopter/project_analyzer.rb +252 -0
  26. data/lib/adopter/project_detector.rb +157 -0
  27. data/lib/commands/adopt_commands.rb +42 -0
  28. data/lib/commands/plugin_commands.rb +0 -2
  29. data/lib/commands/scaffolding_commands.rb +220 -37
  30. data/lib/commands/utility_commands.rb +82 -2
  31. data/lib/generators/automation/automation_generator.rb +0 -7
  32. data/lib/generators/automation/templates/account.tt +9 -5
  33. data/lib/generators/automation/templates/appium_caps.tt +60 -6
  34. data/lib/generators/automation/templates/home.tt +4 -4
  35. data/lib/generators/automation/templates/login.tt +61 -4
  36. data/lib/generators/automation/templates/page.tt +13 -7
  37. data/lib/generators/automation/templates/partials/element.tt +1 -1
  38. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -4
  39. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -8
  40. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -4
  41. data/lib/generators/automation/templates/partials/url_methods.tt +0 -1
  42. data/lib/generators/automation/templates/partials/visit_method.tt +11 -1
  43. data/lib/generators/automation/templates/pdp.tt +1 -1
  44. data/lib/generators/common_generator.rb +12 -0
  45. data/lib/generators/cucumber/cucumber_generator.rb +36 -0
  46. data/lib/generators/cucumber/templates/accessibility_feature.tt +5 -0
  47. data/lib/generators/cucumber/templates/accessibility_steps.tt +21 -0
  48. data/lib/generators/cucumber/templates/cucumber.tt +8 -1
  49. data/lib/generators/cucumber/templates/env.tt +6 -4
  50. data/lib/generators/cucumber/templates/feature.tt +0 -4
  51. data/lib/generators/cucumber/templates/partials/appium_env.tt +5 -0
  52. data/lib/generators/cucumber/templates/partials/capybara_env.tt +38 -0
  53. data/lib/generators/cucumber/templates/partials/capybara_world.tt +6 -0
  54. data/lib/generators/cucumber/templates/partials/driver_world.tt +1 -4
  55. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +2 -2
  56. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -35
  57. data/lib/generators/cucumber/templates/partials/watir_env.tt +20 -1
  58. data/lib/generators/cucumber/templates/partials/web_steps.tt +10 -15
  59. data/lib/generators/cucumber/templates/performance_feature.tt +5 -0
  60. data/lib/generators/cucumber/templates/performance_steps.tt +17 -0
  61. data/lib/generators/cucumber/templates/steps.tt +2 -2
  62. data/lib/generators/cucumber/templates/visual_feature.tt +5 -0
  63. data/lib/generators/cucumber/templates/visual_steps.tt +19 -0
  64. data/lib/generators/cucumber/templates/world.tt +5 -3
  65. data/lib/generators/generator.rb +50 -7
  66. data/lib/generators/helper_generator.rb +39 -9
  67. data/lib/generators/infrastructure/github_generator.rb +6 -0
  68. data/lib/generators/infrastructure/templates/github.tt +12 -8
  69. data/lib/generators/infrastructure/templates/github_appium.tt +108 -0
  70. data/lib/generators/infrastructure/templates/gitlab.tt +6 -3
  71. data/lib/generators/invoke_generators.rb +43 -9
  72. data/lib/generators/menu_generator.rb +122 -11
  73. data/lib/generators/minitest/minitest_generator.rb +35 -0
  74. data/lib/generators/minitest/templates/accessibility_test.tt +26 -0
  75. data/lib/generators/minitest/templates/performance_test.tt +18 -0
  76. data/lib/generators/minitest/templates/test.tt +64 -0
  77. data/lib/generators/minitest/templates/visual_test.tt +23 -0
  78. data/lib/generators/rspec/rspec_generator.rb +16 -4
  79. data/lib/generators/rspec/templates/accessibility_spec.tt +25 -0
  80. data/lib/generators/rspec/templates/performance_spec.tt +18 -0
  81. data/lib/generators/rspec/templates/spec.tt +13 -41
  82. data/lib/generators/rspec/templates/visual_spec.tt +20 -0
  83. data/lib/generators/template_renderer/partial_cache.rb +126 -0
  84. data/lib/generators/template_renderer/partial_resolver.rb +110 -0
  85. data/lib/generators/template_renderer/template_error.rb +50 -0
  86. data/lib/generators/template_renderer.rb +106 -0
  87. data/lib/generators/templates/common/config.tt +2 -2
  88. data/lib/generators/templates/common/gemfile.tt +36 -9
  89. data/lib/generators/templates/common/git_ignore.tt +6 -1
  90. data/lib/generators/templates/common/partials/mobile_config.tt +5 -1
  91. data/lib/generators/templates/common/partials/web_config.tt +17 -8
  92. data/lib/generators/templates/common/rakefile.tt +36 -0
  93. data/lib/generators/templates/common/read_me.tt +43 -91
  94. data/lib/generators/templates/common/rspec.tt +3 -0
  95. data/lib/generators/templates/common/ruby_version.tt +1 -0
  96. data/lib/generators/templates/helpers/allure_helper.tt +13 -2
  97. data/lib/generators/templates/helpers/browser_helper.tt +13 -2
  98. data/lib/generators/templates/helpers/capybara_helper.tt +32 -0
  99. data/lib/generators/templates/helpers/debug_helper.tt +190 -0
  100. data/lib/generators/templates/helpers/driver_helper.tt +3 -11
  101. data/lib/generators/templates/helpers/partials/allure_imports.tt +3 -1
  102. data/lib/generators/templates/helpers/partials/allure_requirements.tt +3 -1
  103. data/lib/generators/templates/helpers/partials/appium_driver.tt +44 -0
  104. data/lib/generators/templates/helpers/partials/browserstack_config.tt +13 -0
  105. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +7 -0
  106. data/lib/generators/templates/helpers/partials/debug_start.tt +7 -0
  107. data/lib/generators/templates/helpers/partials/driver_and_options.tt +5 -115
  108. data/lib/generators/templates/helpers/partials/quit_driver.tt +3 -1
  109. data/lib/generators/templates/helpers/partials/screenshot.tt +3 -1
  110. data/lib/generators/templates/helpers/partials/selenium_driver.tt +26 -0
  111. data/lib/generators/templates/helpers/partials/video_start.tt +9 -0
  112. data/lib/generators/templates/helpers/partials/video_stop.tt +4 -0
  113. data/lib/generators/templates/helpers/performance_helper.tt +57 -0
  114. data/lib/generators/templates/helpers/spec_helper.tt +72 -10
  115. data/lib/generators/templates/helpers/test_helper.tt +94 -0
  116. data/lib/generators/templates/helpers/video_helper.tt +270 -0
  117. data/lib/generators/templates/helpers/visual_helper.tt +39 -46
  118. data/lib/llm/client.rb +79 -0
  119. data/lib/llm/config.rb +57 -0
  120. data/lib/llm/prompts.rb +84 -0
  121. data/lib/llm/provider.rb +27 -0
  122. data/lib/llm/providers/anthropic_provider.rb +43 -0
  123. data/lib/llm/providers/ollama_provider.rb +56 -0
  124. data/lib/llm/providers/openai_provider.rb +42 -0
  125. data/lib/llm/response_parser.rb +67 -0
  126. data/lib/plugin/plugin.rb +22 -20
  127. data/lib/plugin/plugin_exposer.rb +16 -38
  128. data/lib/ruby_raider.rb +51 -11
  129. data/lib/scaffolding/crud_generator.rb +94 -0
  130. data/lib/scaffolding/dry_run_presenter.rb +16 -0
  131. data/lib/scaffolding/name_normalizer.rb +63 -0
  132. data/lib/scaffolding/page_introspector.rb +45 -0
  133. data/lib/scaffolding/project_detector.rb +72 -0
  134. data/lib/scaffolding/scaffold_menu.rb +103 -0
  135. data/lib/scaffolding/scaffolding.rb +158 -11
  136. data/lib/scaffolding/templates/component.tt +30 -0
  137. data/lib/scaffolding/templates/feature.tt +4 -4
  138. data/lib/scaffolding/templates/helper.tt +15 -1
  139. data/lib/scaffolding/templates/page_from_url.tt +75 -0
  140. data/lib/scaffolding/templates/page_object.tt +50 -1
  141. data/lib/scaffolding/templates/spec.tt +33 -2
  142. data/lib/scaffolding/templates/spec_from_page.tt +31 -0
  143. data/lib/scaffolding/templates/spec_from_url.tt +46 -0
  144. data/lib/scaffolding/templates/steps.tt +17 -5
  145. data/lib/scaffolding/url_analyzer.rb +179 -0
  146. data/lib/utilities/desktop_downloader.rb +177 -0
  147. data/lib/utilities/logo.rb +83 -0
  148. data/lib/utilities/utilities.rb +53 -20
  149. data/lib/version +1 -1
  150. data/ruby_raider.gemspec +1 -0
  151. data/sig/adopter/adopt_menu.rbs +25 -0
  152. data/sig/adopter/converters/base_converter.rbs +23 -0
  153. data/sig/adopter/converters/identity_converter.rbs +16 -0
  154. data/sig/adopter/migration_plan.rbs +34 -0
  155. data/sig/adopter/migrator.rbs +21 -0
  156. data/sig/adopter/plan_builder.rbs +38 -0
  157. data/sig/adopter/project_analyzer.rbs +39 -0
  158. data/sig/adopter/project_detector.rbs +26 -0
  159. data/sig/commands/adopt_commands.rbs +8 -0
  160. data/sig/commands/loaded_commands.rbs +5 -0
  161. data/sig/commands/plugin_commands.rbs +9 -0
  162. data/sig/commands/scaffolding_commands.rbs +28 -0
  163. data/sig/commands/utility_commands.rbs +21 -0
  164. data/sig/generators/automation/automation_generator.rbs +20 -0
  165. data/sig/generators/common_generator.rbs +12 -0
  166. data/sig/generators/cucumber/cucumber_generator.rbs +16 -0
  167. data/sig/generators/generator.rbs +40 -0
  168. data/sig/generators/helper_generator.rbs +18 -0
  169. data/sig/generators/infrastructure/github_generator.rbs +5 -0
  170. data/sig/generators/infrastructure/gitlab_generator.rbs +4 -0
  171. data/sig/generators/invoke_generators.rbs +10 -0
  172. data/sig/generators/menu_generator.rbs +29 -0
  173. data/sig/generators/minitest/minitest_generator.rbs +8 -0
  174. data/sig/generators/rspec/rspec_generator.rbs +8 -0
  175. data/sig/generators/template_renderer/partial_cache.rbs +20 -0
  176. data/sig/generators/template_renderer/partial_resolver.rbs +20 -0
  177. data/sig/generators/template_renderer/template_error.rbs +19 -0
  178. data/sig/generators/template_renderer.rbs +10 -0
  179. data/sig/llm/client.rbs +15 -0
  180. data/sig/llm/config.rbs +20 -0
  181. data/sig/llm/prompts.rbs +8 -0
  182. data/sig/llm/provider.rbs +12 -0
  183. data/sig/llm/providers/anthropic_provider.rbs +16 -0
  184. data/sig/llm/providers/ollama_provider.rbs +18 -0
  185. data/sig/llm/providers/openai_provider.rbs +16 -0
  186. data/sig/llm/response_parser.rbs +13 -0
  187. data/sig/plugin/plugin.rbs +24 -0
  188. data/sig/plugin/plugin_exposer.rbs +20 -0
  189. data/sig/ruby_raider.rbs +15 -0
  190. data/sig/scaffolding/crud_generator.rbs +16 -0
  191. data/sig/scaffolding/dry_run_presenter.rbs +4 -0
  192. data/sig/scaffolding/name_normalizer.rbs +17 -0
  193. data/sig/scaffolding/page_introspector.rbs +14 -0
  194. data/sig/scaffolding/project_detector.rbs +14 -0
  195. data/sig/scaffolding/scaffold_menu.rbs +18 -0
  196. data/sig/scaffolding/scaffolding.rbs +55 -0
  197. data/sig/scaffolding/url_analyzer.rbs +28 -0
  198. data/sig/utilities/desktop_downloader.rbs +23 -0
  199. data/sig/utilities/logger.rbs +13 -0
  200. data/sig/utilities/logo.rbs +16 -0
  201. data/sig/utilities/utilities.rbs +30 -0
  202. data/sig/vendor/thor.rbs +34 -0
  203. data/sig/vendor/tty_prompt.rbs +15 -0
  204. data/spec/adopter/adopt_menu_spec.rb +176 -0
  205. data/spec/adopter/converters/identity_converter_spec.rb +145 -0
  206. data/spec/adopter/migration_plan_spec.rb +113 -0
  207. data/spec/adopter/migrator_spec.rb +277 -0
  208. data/spec/adopter/plan_builder_spec.rb +298 -0
  209. data/spec/adopter/project_analyzer_spec.rb +337 -0
  210. data/spec/adopter/project_detector_spec.rb +295 -0
  211. data/spec/commands/raider_commands_spec.rb +129 -0
  212. data/spec/generators/fixtures/templates/test.tt +1 -0
  213. data/spec/generators/fixtures/templates/test_partial.tt +1 -0
  214. data/spec/generators/generator_spec.rb +23 -0
  215. data/spec/generators/template_renderer_spec.rb +298 -0
  216. data/spec/integration/commands/scaffolding_commands_spec.rb +2 -2
  217. data/spec/integration/commands/utility_commands_spec.rb +24 -4
  218. data/spec/integration/content/ci_content_spec.rb +119 -0
  219. data/spec/integration/content/common_content_spec.rb +288 -0
  220. data/spec/integration/content/config_content_spec.rb +175 -0
  221. data/spec/integration/content/content_helper.rb +32 -0
  222. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  223. data/spec/integration/content/helper_content_spec.rb +485 -0
  224. data/spec/integration/content/page_content_spec.rb +259 -0
  225. data/spec/integration/content/reporter_content_spec.rb +236 -0
  226. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  227. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  228. data/spec/integration/content/test_content_spec.rb +266 -0
  229. data/spec/integration/end_to_end_features_spec.rb +690 -0
  230. data/spec/integration/end_to_end_spec.rb +361 -0
  231. data/spec/integration/generators/automation_generator_spec.rb +9 -21
  232. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  233. data/spec/integration/generators/common_generator_spec.rb +48 -49
  234. data/spec/integration/generators/config_features_spec.rb +155 -0
  235. data/spec/integration/generators/cucumber_generator_spec.rb +7 -7
  236. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  237. data/spec/integration/generators/github_generator_spec.rb +8 -8
  238. data/spec/integration/generators/gitlab_generator_spec.rb +8 -8
  239. data/spec/integration/generators/helpers_generator_spec.rb +70 -44
  240. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  241. data/spec/integration/generators/minitest_generator_spec.rb +64 -0
  242. data/spec/integration/generators/reporter_spec.rb +159 -0
  243. data/spec/integration/generators/rspec_generator_spec.rb +7 -7
  244. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  245. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  246. data/spec/integration/settings_helper.rb +1 -4
  247. data/spec/integration/spec_helper.rb +46 -11
  248. data/spec/llm/client_spec.rb +79 -0
  249. data/spec/llm/config_spec.rb +92 -0
  250. data/spec/llm/prompts_spec.rb +49 -0
  251. data/spec/llm/response_parser_spec.rb +92 -0
  252. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  253. data/spec/menus/menu_generator_spec.rb +263 -0
  254. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  255. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  256. data/spec/scaffolding/scaffold_project_detector_spec.rb +104 -0
  257. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  258. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  259. data/spec/system/adopt_matrix_spec.rb +537 -0
  260. data/spec/system/adopt_spec.rb +225 -0
  261. data/spec/system/capybara_spec.rb +42 -0
  262. data/spec/system/selenium_spec.rb +19 -17
  263. data/spec/system/support/system_test_helper.rb +33 -0
  264. data/spec/system/watir_spec.rb +19 -17
  265. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  266. metadata +193 -18
  267. data/.github/workflows/push_gem.yml +0 -37
  268. data/.github/workflows/selenium.yml +0 -22
  269. data/.github/workflows/watir.yml +0 -22
  270. data/lib/generators/automation/templates/partials/android_caps.tt +0 -17
  271. data/lib/generators/automation/templates/partials/cross_platform_caps.tt +0 -25
  272. data/lib/generators/automation/templates/partials/ios_caps.tt +0 -18
  273. data/lib/generators/automation/templates/partials/selenium_account.tt +0 -9
  274. data/lib/generators/automation/templates/partials/selenium_login.tt +0 -34
  275. data/lib/generators/automation/templates/partials/watir_account.tt +0 -7
  276. data/lib/generators/automation/templates/partials/watir_login.tt +0 -32
  277. data/lib/generators/automation/templates/visual_options.tt +0 -16
  278. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -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
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'yaml'
5
+ require_relative 'name_normalizer'
6
+ require_relative 'project_detector'
4
7
 
5
8
  class Scaffolding < Thor::Group
6
9
  include Thor::Actions
@@ -8,28 +11,48 @@ class Scaffolding < Thor::Group
8
11
  argument :name, optional: true
9
12
  argument :path, optional: true
10
13
 
14
+ attr_writer :uses
15
+
16
+ OVERRIDE_DIR = '.ruby_raider/templates'
17
+
11
18
  def self.source_root
12
19
  "#{File.dirname(__FILE__)}/templates"
13
20
  end
14
21
 
22
+ # Check for user template override before using default
23
+ def template(source, *args, &block)
24
+ override = File.join(OVERRIDE_DIR, File.basename(source))
25
+ if File.exist?(override)
26
+ super(File.expand_path(override), *args, &block)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ # --- Generation methods ---
33
+
15
34
  def generate_page
16
- template('page_object.tt', default_path("page_objects/pages/#{name}.rb", '_page.rb'))
35
+ template('page_object.tt', default_path("page_objects/pages/#{normalized_name}.rb", '_page.rb'))
17
36
  end
18
37
 
19
38
  def generate_feature
20
- template('feature.tt', default_path("features/#{name}.feature", '.feature'))
39
+ template('feature.tt', default_path("features/#{normalized_name}.feature", '.feature'))
21
40
  end
22
41
 
23
42
  def generate_spec
24
- template('spec.tt', default_path("spec/#{name}_page_spec.rb", '_spec.rb'))
43
+ template('spec.tt', default_path("spec/#{normalized_name}_page_spec.rb", '_spec.rb'))
25
44
  end
26
45
 
27
46
  def generate_helper
28
- template('helper.tt', default_path("helpers/#{name}_helper.rb", '_helper.rb'))
47
+ template('helper.tt', default_path("helpers/#{normalized_name}_helper.rb", '_helper.rb'))
29
48
  end
30
49
 
31
50
  def generate_steps
32
- template('steps.tt', default_path("features/step_definitions/#{name}_steps.rb", '_steps.rb'))
51
+ template('steps.tt', default_path("features/step_definitions/#{normalized_name}_steps.rb", '_steps.rb'))
52
+ end
53
+
54
+ def generate_component
55
+ template('component.tt', default_path("page_objects/components/#{normalized_name}.rb", '.rb'))
33
56
  end
34
57
 
35
58
  def generate_config
@@ -37,31 +60,155 @@ class Scaffolding < Thor::Group
37
60
  default_path('config/config.yml', '.yml'))
38
61
  end
39
62
 
63
+ def generate_spec_from_page(source_file, ai: false) # rubocop:disable Naming/MethodParameterName
64
+ require_relative 'page_introspector'
65
+ @introspected = PageIntrospector.new(source_file)
66
+ enrich_with_ai_scenarios if ai
67
+ template('spec_from_page.tt', "spec/#{normalized_name}_spec.rb")
68
+ end
69
+
70
+ def generate_page_from_url(analysis)
71
+ @url_data = analysis
72
+ template('page_from_url.tt', "page_objects/pages/#{normalized_name}.rb")
73
+ end
74
+
75
+ def generate_spec_from_url(analysis)
76
+ @url_data = analysis
77
+ template('spec_from_url.tt', "spec/#{normalized_name}_spec.rb")
78
+ end
79
+
80
+ # --- Deletion methods ---
81
+
40
82
  def delete_page
41
- remove_file(default_path("page_objects/pages/#{name}.rb", '_page.rb'))
83
+ remove_file(default_path("page_objects/pages/#{normalized_name}.rb", '_page.rb'))
42
84
  end
43
85
 
44
86
  def delete_feature
45
- remove_file(default_path("features/#{name}.feature", '.feature'))
87
+ remove_file(default_path("features/#{normalized_name}.feature", '.feature'))
46
88
  end
47
89
 
48
90
  def delete_spec
49
- remove_file(default_path("spec/#{name}_page_spec.rb", '_spec.rb'))
91
+ remove_file(default_path("spec/#{normalized_name}_page_spec.rb", '_spec.rb'))
50
92
  end
51
93
 
52
94
  def delete_helper
53
- remove_file(default_path("helpers/#{name}_helper.rb", '_helper.rb'))
95
+ remove_file(default_path("helpers/#{normalized_name}_helper.rb", '_helper.rb'))
54
96
  end
55
97
 
56
98
  def delete_steps
57
- remove_file(default_path("features/step_definitions/#{name}_steps.rb", '_steps.rb'))
99
+ remove_file(default_path("features/step_definitions/#{normalized_name}_steps.rb", '_steps.rb'))
100
+ end
101
+
102
+ def delete_component
103
+ remove_file(default_path("page_objects/components/#{normalized_name}.rb", '.rb'))
58
104
  end
59
105
 
60
106
  def delete_config
61
107
  remove_file(default_path('config/config.yml', '.yml'))
62
108
  end
63
109
 
110
+ # --- Path planning (for dry-run) ---
111
+
112
+ def self.planned_path(name, type, custom_path = nil)
113
+ n = NameNormalizer.normalize(name)
114
+ case type.to_s
115
+ when 'page' then custom_path ? "#{custom_path}/#{n}_page.rb" : "page_objects/pages/#{n}.rb"
116
+ when 'spec' then custom_path ? "#{custom_path}/#{n}_spec.rb" : "spec/#{n}_page_spec.rb"
117
+ when 'feature' then custom_path ? "#{custom_path}/#{n}.feature" : "features/#{n}.feature"
118
+ when 'steps' then custom_path ? "#{custom_path}/#{n}_steps.rb" : "features/step_definitions/#{n}_steps.rb"
119
+ when 'helper' then custom_path ? "#{custom_path}/#{n}_helper.rb" : "helpers/#{n}_helper.rb"
120
+ when 'component' then custom_path ? "#{custom_path}/#{n}.rb" : "page_objects/components/#{n}.rb"
121
+ when 'model' then "models/data/#{n}.yml"
122
+ end
123
+ end
124
+
125
+ # --- Template helpers (available in .tt files) ---
126
+
127
+ def class_name
128
+ NameNormalizer.to_class_name(name)
129
+ end
130
+
131
+ def page_class_name
132
+ NameNormalizer.to_page_class(name)
133
+ end
134
+
135
+ def normalized_name
136
+ NameNormalizer.normalize(name)
137
+ end
138
+
139
+ def nested?
140
+ NameNormalizer.nested?(name)
141
+ end
142
+
143
+ def module_parts
144
+ NameNormalizer.module_parts(name)
145
+ end
146
+
147
+ def leaf_name
148
+ NameNormalizer.leaf_name(name)
149
+ end
150
+
151
+ def automation_type
152
+ ScaffoldProjectDetector.detect_automation
153
+ end
154
+
155
+ def framework_type
156
+ ScaffoldProjectDetector.detect_framework
157
+ end
158
+
159
+ def project_config
160
+ ScaffoldProjectDetector.config
161
+ end
162
+
163
+ def selenium?
164
+ automation_type == 'selenium'
165
+ end
166
+
167
+ def capybara?
168
+ automation_type == 'capybara'
169
+ end
170
+
171
+ def watir?
172
+ automation_type == 'watir'
173
+ end
174
+
175
+ def uses_list
176
+ Array(@uses).reject(&:empty?)
177
+ end
178
+
179
+ attr_reader :introspected, :url_data
180
+
181
+ def ai_scenarios
182
+ @ai_scenarios || {}
183
+ end
184
+
64
185
  def default_path(standard_path, file_type)
65
- path ? "#{path}/#{name}#{file_type}" : standard_path
186
+ path ? "#{path}/#{normalized_name}#{file_type}" : standard_path
187
+ end
188
+
189
+ private
190
+
191
+ def enrich_with_ai_scenarios
192
+ require_relative '../llm/client'
193
+ require_relative '../llm/prompts'
194
+ require_relative '../llm/response_parser'
195
+
196
+ return unless Llm::Client.available?
197
+
198
+ response = Llm::Client.complete(
199
+ Llm::Prompts.generate_test_scenarios(
200
+ @introspected.class_name,
201
+ @introspected.methods,
202
+ automation_type,
203
+ framework_type
204
+ ),
205
+ system_prompt: Llm::Prompts.system_prompt
206
+ )
207
+ scenarios = Llm::ResponseParser.extract_scenarios(response)
208
+ return unless scenarios
209
+
210
+ @ai_scenarios = scenarios.each_with_object({}) do |s, hash|
211
+ hash[s[:method]] = s
212
+ end
66
213
  end
67
214
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../abstract/component'
4
+
5
+ class <%= class_name %> < Component
6
+ <%- if capybara? -%>
7
+
8
+ # Example: find('.selector').text
9
+ <%- elsif selenium? -%>
10
+
11
+ def content
12
+ component.text
13
+ end
14
+
15
+ private
16
+
17
+ # Elements
18
+ # Example: driver.find_element(css: '.selector')
19
+ <%- elsif watir? -%>
20
+
21
+ def content
22
+ component.text
23
+ end
24
+
25
+ private
26
+
27
+ # Elements
28
+ # Example: browser.element(css: '.selector')
29
+ <%- end -%>
30
+ end
@@ -1,6 +1,6 @@
1
- Feature: <%= name.capitalize %>
1
+ Feature: <%= class_name %>
2
2
 
3
3
  Scenario: Scenario name
4
- Given I am
5
- When I do
6
- Then I see
4
+ Given I am on the <%= normalized_name.tr('_', ' ') %> page
5
+ When I perform an action
6
+ Then I see the expected result
@@ -1,3 +1,17 @@
1
- module <%= name.split('_').map {|word| word.capitalize }.join + 'Helper' %>
1
+ # frozen_string_literal: true
2
+
3
+ <% if nested? -%>
4
+ <% module_parts.each_with_index do |mod, i| -%>
5
+ <%= ' ' * i %>module <%= mod %>
6
+ <% end -%>
7
+ <%= ' ' * module_parts.size %>module <%= NameNormalizer.to_class_name(leaf_name) %>Helper
8
+ <%= ' ' * module_parts.size %> # Add your helper code here
9
+ <%= ' ' * module_parts.size %>end
10
+ <% module_parts.each_with_index do |_mod, i| -%>
11
+ <%= ' ' * (module_parts.size - i - 1) %>end
12
+ <% end -%>
13
+ <% else -%>
14
+ module <%= class_name %>Helper
2
15
  # Add your helper code here
3
16
  end
17
+ <% end -%>