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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require_relative '../generators/invoke_generators'
6
+
7
+ module Adopter
8
+ class Migrator
9
+ attr_reader :plan, :results
10
+
11
+ def initialize(plan)
12
+ @plan = plan
13
+ @results = { pages: 0, tests: 0, features: 0, steps: 0, errors: [] }
14
+ end
15
+
16
+ def execute
17
+ generate_skeleton
18
+ write_converted_pages
19
+ write_converted_tests
20
+ write_converted_features
21
+ write_converted_steps
22
+ merge_gemfile
23
+ apply_config_overrides
24
+ results
25
+ end
26
+
27
+ private
28
+
29
+ # Phase A: Generate a standard Raider project skeleton
30
+ def generate_skeleton
31
+ InvokeGenerators.generate_framework(plan.skeleton_structure)
32
+ end
33
+
34
+ # Phase B: Overlay converted user files on top of the skeleton
35
+
36
+ def write_converted_pages
37
+ plan.converted_pages.each do |file|
38
+ write_file(file)
39
+ @results[:pages] += 1
40
+ end
41
+ end
42
+
43
+ def write_converted_tests
44
+ plan.converted_tests.each do |file|
45
+ write_file(file)
46
+ @results[:tests] += 1
47
+ end
48
+ end
49
+
50
+ def write_converted_features
51
+ plan.converted_features.each do |file|
52
+ write_file(file)
53
+ @results[:features] += 1
54
+ end
55
+ end
56
+
57
+ def write_converted_steps
58
+ plan.converted_steps.each do |file|
59
+ write_file(file)
60
+ @results[:steps] += 1
61
+ end
62
+ end
63
+
64
+ def write_file(converted_file)
65
+ FileUtils.mkdir_p(File.dirname(converted_file.output_path))
66
+ File.write(converted_file.output_path, converted_file.content)
67
+ rescue StandardError => e
68
+ @results[:errors] << "Failed to write #{converted_file.output_path}: #{e.message}"
69
+ end
70
+
71
+ def merge_gemfile
72
+ return if plan.gemfile_additions.empty?
73
+
74
+ gemfile_path = "#{plan.output_path}/Gemfile"
75
+ return unless File.exist?(gemfile_path)
76
+
77
+ content = File.read(gemfile_path)
78
+ additions = plan.gemfile_additions.reject { |gem| content.include?("'#{gem}'") }
79
+ return if additions.empty?
80
+
81
+ gem_lines = additions.map { |gem| "gem '#{gem}'" }.join("\n")
82
+ File.write(gemfile_path, "#{content}\n# Gems from source project\n#{gem_lines}\n")
83
+ end
84
+
85
+ def apply_config_overrides
86
+ return if plan.config_overrides.empty?
87
+
88
+ config_path = "#{plan.output_path}/config/config.yml"
89
+ return unless File.exist?(config_path)
90
+
91
+ config = YAML.safe_load(File.read(config_path), permitted_classes: [Symbol]) || {}
92
+ plan.config_overrides.each { |key, value| config[key.to_s] = value }
93
+ File.write(config_path, YAML.dump(config))
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'migration_plan'
4
+ require_relative 'converters/identity_converter'
5
+
6
+ module Adopter
7
+ class PlanBuilder
8
+ def initialize(analysis, params)
9
+ @analysis = analysis
10
+ @params = params
11
+ @warnings = []
12
+ @manual_actions = []
13
+ end
14
+
15
+ def build
16
+ MigrationPlan.new(
17
+ source_path: @params[:source_path],
18
+ output_path: @params[:output_path],
19
+ target_automation: @params[:target_automation],
20
+ target_framework: @params[:target_framework],
21
+ ci_platform: @params[:ci_platform],
22
+ skeleton_structure: build_skeleton_structure,
23
+ converted_pages: plan_page_conversions,
24
+ converted_tests: plan_test_conversions,
25
+ converted_features: plan_feature_conversions,
26
+ converted_steps: plan_step_conversions,
27
+ gemfile_additions: @analysis[:custom_gems] || [],
28
+ config_overrides: extract_config_overrides,
29
+ warnings: @warnings,
30
+ manual_actions: @manual_actions
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ def build_skeleton_structure
37
+ {
38
+ automation: @params[:target_automation],
39
+ framework: @params[:target_framework],
40
+ name: @params[:output_path],
41
+ ci_platform: @params[:ci_platform]
42
+ }
43
+ end
44
+
45
+ def plan_page_conversions
46
+ pages = @analysis[:pages] || []
47
+ pages.map do |page|
48
+ source_file = File.join(@params[:source_path], page[:path])
49
+ content = File.read(source_file)
50
+ converted = convert_page(content, page)
51
+
52
+ ConvertedFile.new(
53
+ output_path: raider_page_path(page[:class_name]),
54
+ content: converted,
55
+ source_file: page[:path],
56
+ conversion_notes: page_conversion_notes
57
+ )
58
+ end
59
+ end
60
+
61
+ def plan_test_conversions
62
+ tests = @analysis[:tests] || []
63
+ return [] if target_cucumber?
64
+
65
+ tests.reject { |t| t[:type] == :cucumber }.map do |test|
66
+ source_file = File.join(@params[:source_path], test[:path])
67
+ content = File.read(source_file)
68
+ converted = convert_test(content, test)
69
+
70
+ ConvertedFile.new(
71
+ output_path: raider_test_path(test),
72
+ content: converted,
73
+ source_file: test[:path],
74
+ conversion_notes: test_conversion_notes(test[:type])
75
+ )
76
+ end
77
+ end
78
+
79
+ def plan_feature_conversions
80
+ return [] unless target_cucumber?
81
+
82
+ features = @analysis[:features] || []
83
+ features.map do |feature|
84
+ source_file = File.join(@params[:source_path], feature[:path])
85
+ content = File.read(source_file)
86
+
87
+ ConvertedFile.new(
88
+ output_path: raider_feature_path(feature[:path]),
89
+ content:,
90
+ source_file: feature[:path],
91
+ conversion_notes: 'Feature file copied as-is'
92
+ )
93
+ end
94
+ end
95
+
96
+ def plan_step_conversions
97
+ return [] unless target_cucumber?
98
+
99
+ steps = @analysis[:step_definitions] || []
100
+ steps.map do |step|
101
+ source_file = File.join(@params[:source_path], step[:path])
102
+ content = File.read(source_file)
103
+ converted = convert_step_page_references(content)
104
+
105
+ ConvertedFile.new(
106
+ output_path: raider_step_path(step[:path]),
107
+ content: converted,
108
+ source_file: step[:path],
109
+ conversion_notes: 'Step definitions with updated page references'
110
+ )
111
+ end
112
+ end
113
+
114
+ # --- Page conversion ---
115
+
116
+ def convert_page(content, page)
117
+ source_dsl = @analysis[:source_dsl]
118
+ target = @params[:target_automation]
119
+
120
+ if same_automation_dsl?(source_dsl, target)
121
+ restructure_page(content)
122
+ else
123
+ convert_page_dsl(content, page, source_dsl, target)
124
+ end
125
+ end
126
+
127
+ def convert_page_dsl(content, page, source_dsl, target)
128
+ converter = find_page_converter(source_dsl, target)
129
+ if converter
130
+ converter.call(content, page)
131
+ else
132
+ add_warning("No converter for #{source_dsl} → #{target}. Page '#{page[:class_name]}' copied as-is.")
133
+ restructure_page(content)
134
+ end
135
+ end
136
+
137
+ def find_page_converter(source_dsl, target)
138
+ key = :"#{normalize_dsl(source_dsl)}_to_#{target}"
139
+ page_converters[key]
140
+ end
141
+
142
+ def page_converters
143
+ @page_converters ||= {}
144
+ end
145
+
146
+ def register_page_converter(key, converter)
147
+ page_converters[key] = converter
148
+ end
149
+
150
+ # --- Test conversion ---
151
+
152
+ def convert_test(content, test)
153
+ source_framework = test[:type]
154
+ target = @params[:target_framework]
155
+
156
+ if source_framework.to_s == target
157
+ restructure_test(content)
158
+ else
159
+ convert_test_framework(content, test, source_framework, target)
160
+ end
161
+ end
162
+
163
+ def convert_test_framework(content, test, source_framework, target)
164
+ converter = find_test_converter(source_framework, target)
165
+ if converter
166
+ converter.call(content, test)
167
+ else
168
+ add_warning("No converter for #{source_framework} → #{target}. Test '#{test[:path]}' copied as-is.")
169
+ restructure_test(content)
170
+ end
171
+ end
172
+
173
+ def find_test_converter(source_framework, target)
174
+ key = :"#{source_framework}_to_#{target}"
175
+ test_converters[key]
176
+ end
177
+
178
+ def test_converters
179
+ @test_converters ||= {}
180
+ end
181
+
182
+ # --- Restructuring (identity conversion) ---
183
+
184
+ def identity_converter
185
+ @identity_converter ||= Converters::IdentityConverter.new(@params[:target_automation])
186
+ end
187
+
188
+ def restructure_page(content)
189
+ identity_converter.convert_page(content, {})
190
+ end
191
+
192
+ def restructure_test(content)
193
+ identity_converter.convert_test(content, {})
194
+ end
195
+
196
+ def convert_step_page_references(content)
197
+ identity_converter.convert_step(content)
198
+ end
199
+
200
+ # --- Path helpers ---
201
+
202
+ def raider_page_path(class_name)
203
+ filename = class_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
204
+ filename = filename.delete_suffix('_page')
205
+ "#{@params[:output_path]}/page_objects/pages/#{filename}.rb"
206
+ end
207
+
208
+ def raider_test_path(test)
209
+ basename = File.basename(test[:path], '.rb')
210
+ target = @params[:target_framework]
211
+
212
+ case target
213
+ when 'rspec'
214
+ name = basename.delete_prefix('test_').delete_suffix('_spec')
215
+ "#{@params[:output_path]}/spec/#{name}_spec.rb"
216
+ when 'minitest'
217
+ name = basename.delete_prefix('test_').delete_suffix('_test').delete_suffix('_spec')
218
+ "#{@params[:output_path]}/test/test_#{name}.rb"
219
+ else
220
+ "#{@params[:output_path]}/spec/#{basename}.rb"
221
+ end
222
+ end
223
+
224
+ def raider_feature_path(source_path)
225
+ filename = File.basename(source_path)
226
+ "#{@params[:output_path]}/features/#{filename}"
227
+ end
228
+
229
+ def raider_step_path(source_path)
230
+ filename = File.basename(source_path)
231
+ "#{@params[:output_path]}/features/step_definitions/#{filename}"
232
+ end
233
+
234
+ # --- Config ---
235
+
236
+ def extract_config_overrides
237
+ overrides = {}
238
+ overrides[:browser] = @params[:browser] || @analysis[:browser]
239
+ overrides[:url] = @params[:url] || @analysis[:url]
240
+ overrides.compact
241
+ end
242
+
243
+ # --- Helpers ---
244
+
245
+ def same_automation_dsl?(source_dsl, target)
246
+ normalize_dsl(source_dsl) == target
247
+ end
248
+
249
+ def normalize_dsl(dsl)
250
+ case dsl
251
+ when :site_prism then 'capybara'
252
+ else dsl.to_s
253
+ end
254
+ end
255
+
256
+ def target_cucumber?
257
+ @params[:target_framework] == 'cucumber'
258
+ end
259
+
260
+ def page_conversion_notes
261
+ source = @analysis[:source_dsl]
262
+ target = @params[:target_automation]
263
+ same_automation_dsl?(source, target) ? 'Restructured to Raider conventions' : "Converted from #{source} to #{target}"
264
+ end
265
+
266
+ def test_conversion_notes(source_type)
267
+ target = @params[:target_framework]
268
+ source_type.to_s == target ? 'Restructured to Raider conventions' : "Converted from #{source_type} to #{target}"
269
+ end
270
+
271
+ def add_warning(message)
272
+ @warnings << message
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'project_detector'
4
+
5
+ module Adopter
6
+ class MobileProjectError < StandardError; end
7
+
8
+ class ProjectAnalyzer
9
+ KNOWN_FRAMEWORK_GEMS = %w[
10
+ activesupport allure-cucumber allure-rspec allure-minitest allure-ruby-commons
11
+ appium_lib appium_console axe-core-rspec axe-core-selenium capybara
12
+ cucumber eyes_selenium eyes_universal minitest minitest-reporters
13
+ parallel_split_test parallel_tests rake reek rspec rubocop rubocop-rspec
14
+ rubocop-minitest ruby_raider selenium-webdriver site_prism watir
15
+ ].freeze
16
+
17
+ PAGE_DSL_PATTERNS = {
18
+ site_prism: /class\s+\w+\s*<\s*SitePrism::Page/,
19
+ capybara: /include\s+Capybara::DSL|\.fill_in\b|\.click_on\b|\.click_button\b/,
20
+ selenium: /\.find_element\s*\(|driver\.navigate/,
21
+ watir: /browser\.text_field|browser\.button|browser\.element/
22
+ }.freeze
23
+
24
+ PAGE_CLASS_PATTERNS = [
25
+ /class\s+\w+\s*<\s*SitePrism::Page/,
26
+ /class\s+\w+\s*<\s*(?:Page|BasePage|AbstractPage)/,
27
+ /include\s+Capybara::DSL/,
28
+ /\.find_element\s*\(/,
29
+ /browser\.text_field|browser\.button/,
30
+ /def\s+(?:visit|login|click_|fill_|select_|navigate)/
31
+ ].freeze
32
+
33
+ TEST_FILE_PATTERNS = {
34
+ rspec: { glob: '**/*_spec.rb', marker: /\b(?:describe|context|it)\b/ },
35
+ cucumber_feature: { glob: '**/*.feature', marker: /\b(?:Feature|Scenario)\b/ },
36
+ cucumber_step: { glob: '**/*_steps.rb', marker: /\b(?:Given|When|Then)\b/ },
37
+ minitest: { glob: '**/test_*.rb', marker: /class\s+\w+\s*<\s*Minitest::Test/ },
38
+ minitest_alt: { glob: '**/*_test.rb', marker: /class\s+\w+\s*<\s*Minitest::Test/ }
39
+ }.freeze
40
+
41
+ HELPER_ROLES = {
42
+ driver: /module\s+DriverHelper|def\s+(?:driver|create_driver)\b/,
43
+ browser: /module\s+BrowserHelper|def\s+(?:browser|create_browser)\b/,
44
+ capybara: /Capybara\.configure|Capybara\.register_driver/,
45
+ env: /^(?:Before|After)\s+do\b/,
46
+ factory: /FactoryBot|ModelFactory|def\s+self\.for\b/,
47
+ spec_helper: /RSpec\.configure|Minitest::Test/
48
+ }.freeze
49
+
50
+ def initialize(source_path, overrides = {})
51
+ @source_path = source_path
52
+ @overrides = overrides
53
+ end
54
+
55
+ def analyze
56
+ detection = ProjectDetector.detect(@source_path)
57
+ validate_web_only!(detection)
58
+
59
+ {
60
+ **detection,
61
+ **@overrides.compact,
62
+ pages: discover_pages,
63
+ tests: discover_tests,
64
+ helpers: discover_helpers,
65
+ features: discover_features,
66
+ step_definitions: discover_step_definitions,
67
+ custom_gems: discover_custom_gems,
68
+ source_dsl: detect_page_dsl
69
+ }
70
+ end
71
+
72
+ private
73
+
74
+ def validate_web_only!(detection)
75
+ return unless detection[:automation] == 'appium'
76
+
77
+ raise MobileProjectError,
78
+ 'Mobile (Appium) projects cannot be adopted. Only web-based projects are supported.'
79
+ end
80
+
81
+ def discover_pages
82
+ page_path = detect_page_dir
83
+ return [] unless page_path
84
+
85
+ ruby_files_in(page_path).filter_map do |file|
86
+ content = File.read(file)
87
+ next unless page_like?(content)
88
+
89
+ {
90
+ path: relative_path(file),
91
+ class_name: extract_class_name(content),
92
+ base_class: extract_base_class(content),
93
+ methods: extract_public_methods(content)
94
+ }
95
+ end
96
+ end
97
+
98
+ def discover_tests
99
+ results = []
100
+ TEST_FILE_PATTERNS.each do |type, config|
101
+ Dir.glob(File.join(@source_path, config[:glob])).each do |file|
102
+ content = File.read(file)
103
+ next unless content.match?(config[:marker])
104
+
105
+ results << {
106
+ path: relative_path(file),
107
+ type: type.to_s.split('_').first.to_sym,
108
+ class_name: extract_class_name(content),
109
+ test_methods: extract_test_methods(content, type)
110
+ }
111
+ end
112
+ end
113
+ results
114
+ end
115
+
116
+ def discover_helpers
117
+ helper_files = Dir.glob(File.join(@source_path, '**', '*helper*.rb')) +
118
+ Dir.glob(File.join(@source_path, '**', 'support', '*.rb')) +
119
+ Dir.glob(File.join(@source_path, '**', 'env.rb'))
120
+
121
+ helper_files.uniq.first(30).filter_map do |file|
122
+ next unless File.file?(file)
123
+
124
+ content = File.read(file)
125
+ role = detect_helper_role(content)
126
+
127
+ {
128
+ path: relative_path(file),
129
+ role:,
130
+ modules_defined: extract_modules(content)
131
+ }
132
+ end
133
+ end
134
+
135
+ def discover_features
136
+ Dir.glob(File.join(@source_path, '**', '*.feature')).map do |file|
137
+ {
138
+ path: relative_path(file),
139
+ scenarios: count_scenarios(File.read(file))
140
+ }
141
+ end
142
+ end
143
+
144
+ def discover_step_definitions
145
+ Dir.glob(File.join(@source_path, '**', '*_steps.rb')).map do |file|
146
+ {
147
+ path: relative_path(file),
148
+ steps: count_steps(File.read(file))
149
+ }
150
+ end
151
+ end
152
+
153
+ def discover_custom_gems
154
+ gemfile = File.join(@source_path, 'Gemfile')
155
+ return [] unless File.exist?(gemfile)
156
+
157
+ ProjectDetector.send(:parse_gemfile, @source_path).reject do |gem_name|
158
+ KNOWN_FRAMEWORK_GEMS.include?(gem_name)
159
+ end
160
+ end
161
+
162
+ def detect_page_dsl
163
+ page_files = discover_pages.map { |p| File.join(@source_path, p[:path]) }
164
+ return :raw if page_files.empty?
165
+
166
+ page_contents = page_files.first(20).map { |f| File.read(f) }.join("\n")
167
+
168
+ PAGE_DSL_PATTERNS.each do |dsl, pattern|
169
+ return dsl if page_contents.match?(pattern)
170
+ end
171
+
172
+ :raw
173
+ end
174
+
175
+ # --- File discovery helpers ---
176
+
177
+ def detect_page_dir
178
+ detected = ProjectDetector.detect_page_path(@source_path)
179
+ return File.join(@source_path, detected) if detected
180
+
181
+ # Fallback: scan all .rb files for page-like classes
182
+ nil
183
+ end
184
+
185
+ def ruby_files_in(dir)
186
+ Dir.glob(File.join(dir, '**', '*.rb'))
187
+ end
188
+
189
+ def relative_path(absolute)
190
+ absolute.sub("#{@source_path}/", '')
191
+ end
192
+
193
+ # --- Content analysis helpers ---
194
+
195
+ def page_like?(content)
196
+ PAGE_CLASS_PATTERNS.any? { |pattern| content.match?(pattern) }
197
+ end
198
+
199
+ def extract_class_name(content)
200
+ match = content.match(/class\s+(\w+)/)
201
+ match&.[](1)
202
+ end
203
+
204
+ def extract_base_class(content)
205
+ match = content.match(/class\s+\w+\s*<\s*([\w:]+)/)
206
+ match&.[](1)
207
+ end
208
+
209
+ def extract_public_methods(content)
210
+ in_private = false
211
+ content.each_line.filter_map do |line|
212
+ in_private = true if line.match?(/^\s*private\b/)
213
+ next if in_private
214
+
215
+ match = line.match(/^\s*def\s+(\w+)/)
216
+ match[1] if match && match[1] != 'initialize'
217
+ end
218
+ end
219
+
220
+ def extract_test_methods(content, type)
221
+ case type
222
+ when :rspec
223
+ content.scan(/\bit\s+['"]([^'"]+)['"]/).flatten
224
+ when :minitest, :minitest_alt
225
+ content.scan(/def\s+(test_\w+)/).flatten
226
+ when :cucumber_step
227
+ content.scan(/(?:Given|When|Then)\(['"]([^'"]+)['"]/).flatten
228
+ else
229
+ []
230
+ end
231
+ end
232
+
233
+ def extract_modules(content)
234
+ content.scan(/module\s+(\w+)/).flatten
235
+ end
236
+
237
+ def detect_helper_role(content)
238
+ HELPER_ROLES.each do |role, pattern|
239
+ return role if content.match?(pattern)
240
+ end
241
+ :custom
242
+ end
243
+
244
+ def count_scenarios(content)
245
+ content.scan(/^\s*Scenario/).length
246
+ end
247
+
248
+ def count_steps(content)
249
+ content.scan(/^\s*(?:Given|When|Then|And|But)\b/).length
250
+ end
251
+ end
252
+ end