ruby_raider 1.1.4 → 2.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/integration.yml +4 -6
  3. data/.github/workflows/reek.yml +6 -5
  4. data/.github/workflows/release.yml +175 -0
  5. data/.github/workflows/rubocop.yml +7 -6
  6. data/.github/workflows/system_tests.yml +83 -0
  7. data/.gitignore +1 -1
  8. data/.rubocop.yml +24 -0
  9. data/README.md +3 -1
  10. data/RELEASE.md +412 -0
  11. data/RELEASE_QUICK_GUIDE.md +77 -0
  12. data/bin/release +186 -0
  13. data/lib/adopter/adopt_menu.rb +150 -0
  14. data/lib/adopter/converters/base_converter.rb +85 -0
  15. data/lib/adopter/converters/identity_converter.rb +56 -0
  16. data/lib/adopter/migration_plan.rb +75 -0
  17. data/lib/adopter/migrator.rb +96 -0
  18. data/lib/adopter/plan_builder.rb +278 -0
  19. data/lib/adopter/project_analyzer.rb +256 -0
  20. data/lib/adopter/project_detector.rb +159 -0
  21. data/lib/commands/adopt_commands.rb +43 -0
  22. data/lib/generators/automation/templates/account.tt +9 -5
  23. data/lib/generators/automation/templates/appium_caps.tt +60 -6
  24. data/lib/generators/automation/templates/home.tt +4 -4
  25. data/lib/generators/automation/templates/login.tt +61 -4
  26. data/lib/generators/automation/templates/page.tt +13 -7
  27. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -4
  28. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -1
  29. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -4
  30. data/lib/generators/automation/templates/partials/visit_method.tt +11 -1
  31. data/lib/generators/automation/templates/pdp.tt +1 -1
  32. data/lib/generators/cucumber/templates/env.tt +6 -4
  33. data/lib/generators/cucumber/templates/partials/capybara_env.tt +20 -0
  34. data/lib/generators/cucumber/templates/partials/capybara_world.tt +6 -0
  35. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +2 -2
  36. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -3
  37. data/lib/generators/cucumber/templates/steps.tt +2 -2
  38. data/lib/generators/cucumber/templates/world.tt +5 -3
  39. data/lib/generators/generator.rb +14 -2
  40. data/lib/generators/helper_generator.rb +16 -3
  41. data/lib/generators/infrastructure/github_generator.rb +6 -0
  42. data/lib/generators/infrastructure/templates/github.tt +11 -7
  43. data/lib/generators/infrastructure/templates/github_appium.tt +108 -0
  44. data/lib/generators/infrastructure/templates/gitlab.tt +5 -2
  45. data/lib/generators/invoke_generators.rb +1 -0
  46. data/lib/generators/menu_generator.rb +2 -0
  47. data/lib/generators/minitest/minitest_generator.rb +23 -0
  48. data/lib/generators/minitest/templates/test.tt +93 -0
  49. data/lib/generators/rspec/templates/spec.tt +12 -10
  50. data/lib/generators/template_renderer/partial_cache.rb +116 -0
  51. data/lib/generators/template_renderer/partial_resolver.rb +103 -0
  52. data/lib/generators/template_renderer/template_error.rb +50 -0
  53. data/lib/generators/template_renderer.rb +90 -0
  54. data/lib/generators/templates/common/config.tt +2 -2
  55. data/lib/generators/templates/common/gemfile.tt +15 -3
  56. data/lib/generators/templates/common/partials/web_config.tt +1 -1
  57. data/lib/generators/templates/common/read_me.tt +3 -1
  58. data/lib/generators/templates/helpers/allure_helper.tt +2 -2
  59. data/lib/generators/templates/helpers/browser_helper.tt +1 -0
  60. data/lib/generators/templates/helpers/capybara_helper.tt +28 -0
  61. data/lib/generators/templates/helpers/driver_helper.tt +1 -1
  62. data/lib/generators/templates/helpers/partials/allure_imports.tt +3 -1
  63. data/lib/generators/templates/helpers/partials/allure_requirements.tt +3 -1
  64. data/lib/generators/templates/helpers/partials/appium_driver.tt +46 -0
  65. data/lib/generators/templates/helpers/partials/axe_driver.tt +10 -0
  66. data/lib/generators/templates/helpers/partials/browserstack_config.tt +13 -0
  67. data/lib/generators/templates/helpers/partials/driver_and_options.tt +6 -114
  68. data/lib/generators/templates/helpers/partials/quit_driver.tt +3 -1
  69. data/lib/generators/templates/helpers/partials/screenshot.tt +3 -1
  70. data/lib/generators/templates/helpers/partials/selenium_driver.tt +25 -0
  71. data/lib/generators/templates/helpers/spec_helper.tt +17 -4
  72. data/lib/generators/templates/helpers/test_helper.tt +26 -0
  73. data/lib/generators/templates/helpers/visual_spec_helper.tt +1 -1
  74. data/lib/ruby_raider.rb +5 -0
  75. data/lib/version +1 -1
  76. data/spec/adopter/adopt_menu_spec.rb +176 -0
  77. data/spec/adopter/converters/identity_converter_spec.rb +145 -0
  78. data/spec/adopter/migration_plan_spec.rb +113 -0
  79. data/spec/adopter/migrator_spec.rb +277 -0
  80. data/spec/adopter/plan_builder_spec.rb +298 -0
  81. data/spec/adopter/project_analyzer_spec.rb +337 -0
  82. data/spec/adopter/project_detector_spec.rb +295 -0
  83. data/spec/generators/fixtures/templates/test.tt +1 -0
  84. data/spec/generators/fixtures/templates/test_partial.tt +1 -0
  85. data/spec/generators/template_renderer_spec.rb +298 -0
  86. data/spec/integration/commands/scaffolding_commands_spec.rb +2 -2
  87. data/spec/integration/commands/utility_commands_spec.rb +2 -2
  88. data/spec/integration/end_to_end_spec.rb +325 -0
  89. data/spec/integration/generators/automation_generator_spec.rb +11 -11
  90. data/spec/integration/generators/common_generator_spec.rb +40 -40
  91. data/spec/integration/generators/cucumber_generator_spec.rb +7 -7
  92. data/spec/integration/generators/github_generator_spec.rb +8 -8
  93. data/spec/integration/generators/gitlab_generator_spec.rb +8 -8
  94. data/spec/integration/generators/helpers_generator_spec.rb +73 -35
  95. data/spec/integration/generators/minitest_generator_spec.rb +70 -0
  96. data/spec/integration/generators/rspec_generator_spec.rb +7 -7
  97. data/spec/integration/settings_helper.rb +1 -1
  98. data/spec/integration/spec_helper.rb +20 -2
  99. data/spec/system/capybara_spec.rb +42 -0
  100. data/spec/system/selenium_spec.rb +19 -17
  101. data/spec/system/support/system_test_helper.rb +35 -0
  102. data/spec/system/watir_spec.rb +19 -17
  103. metadata +46 -16
  104. data/.github/workflows/push_gem.yml +0 -37
  105. data/.github/workflows/selenium.yml +0 -22
  106. data/.github/workflows/watir.yml +0 -22
  107. data/lib/generators/automation/templates/partials/android_caps.tt +0 -17
  108. data/lib/generators/automation/templates/partials/cross_platform_caps.tt +0 -25
  109. data/lib/generators/automation/templates/partials/ios_caps.tt +0 -18
  110. data/lib/generators/automation/templates/partials/selenium_account.tt +0 -9
  111. data/lib/generators/automation/templates/partials/selenium_login.tt +0 -34
  112. data/lib/generators/automation/templates/partials/watir_account.tt +0 -7
  113. data/lib/generators/automation/templates/partials/watir_login.tt +0 -32
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'migration_plan'
4
+ require_relative 'converters/identity_converter'
5
+
6
+ module Adopter
7
+ # :reek:TooManyMethods { enabled: false }
8
+ class PlanBuilder
9
+ def initialize(analysis, params)
10
+ @analysis = analysis
11
+ @params = params
12
+ @warnings = []
13
+ @manual_actions = []
14
+ end
15
+
16
+ def build
17
+ MigrationPlan.new(
18
+ source_path: @params[:source_path],
19
+ output_path: @params[:output_path],
20
+ target_automation: @params[:target_automation],
21
+ target_framework: @params[:target_framework],
22
+ ci_platform: @params[:ci_platform],
23
+ skeleton_structure: build_skeleton_structure,
24
+ converted_pages: plan_page_conversions,
25
+ converted_tests: plan_test_conversions,
26
+ converted_features: plan_feature_conversions,
27
+ converted_steps: plan_step_conversions,
28
+ gemfile_additions: @analysis[:custom_gems] || [],
29
+ config_overrides: extract_config_overrides,
30
+ warnings: @warnings,
31
+ manual_actions: @manual_actions
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def build_skeleton_structure
38
+ {
39
+ automation: @params[:target_automation],
40
+ framework: @params[:target_framework],
41
+ name: @params[:output_path],
42
+ ci_platform: @params[:ci_platform]
43
+ }
44
+ end
45
+
46
+ def plan_page_conversions
47
+ pages = @analysis[:pages] || []
48
+ pages.map do |page|
49
+ source_file = File.join(@params[:source_path], page[:path])
50
+ content = File.read(source_file)
51
+ converted = convert_page(content, page)
52
+
53
+ ConvertedFile.new(
54
+ output_path: raider_page_path(page[:class_name]),
55
+ content: converted,
56
+ source_file: page[:path],
57
+ conversion_notes: page_conversion_notes
58
+ )
59
+ end
60
+ end
61
+
62
+ def plan_test_conversions
63
+ tests = @analysis[:tests] || []
64
+ return [] if target_cucumber?
65
+
66
+ tests.select { |t| t[:type] != :cucumber }.map do |test|
67
+ source_file = File.join(@params[:source_path], test[:path])
68
+ content = File.read(source_file)
69
+ converted = convert_test(content, test)
70
+
71
+ ConvertedFile.new(
72
+ output_path: raider_test_path(test),
73
+ content: converted,
74
+ source_file: test[:path],
75
+ conversion_notes: test_conversion_notes(test[:type])
76
+ )
77
+ end
78
+ end
79
+
80
+ def plan_feature_conversions
81
+ return [] unless target_cucumber?
82
+
83
+ features = @analysis[:features] || []
84
+ features.map do |feature|
85
+ source_file = File.join(@params[:source_path], feature[:path])
86
+ content = File.read(source_file)
87
+
88
+ ConvertedFile.new(
89
+ output_path: raider_feature_path(feature[:path]),
90
+ content: content,
91
+ source_file: feature[:path],
92
+ conversion_notes: 'Feature file copied as-is'
93
+ )
94
+ end
95
+ end
96
+
97
+ def plan_step_conversions
98
+ return [] unless target_cucumber?
99
+
100
+ steps = @analysis[:step_definitions] || []
101
+ steps.map do |step|
102
+ source_file = File.join(@params[:source_path], step[:path])
103
+ content = File.read(source_file)
104
+ converted = convert_step_page_references(content)
105
+
106
+ ConvertedFile.new(
107
+ output_path: raider_step_path(step[:path]),
108
+ content: converted,
109
+ source_file: step[:path],
110
+ conversion_notes: 'Step definitions with updated page references'
111
+ )
112
+ end
113
+ end
114
+
115
+ # --- Page conversion ---
116
+
117
+ def convert_page(content, page)
118
+ source_dsl = @analysis[:source_dsl]
119
+ target = @params[:target_automation]
120
+
121
+ if same_automation_dsl?(source_dsl, target)
122
+ restructure_page(content)
123
+ else
124
+ convert_page_dsl(content, page, source_dsl, target)
125
+ end
126
+ end
127
+
128
+ # :reek:ControlParameter { enabled: false }
129
+ def convert_page_dsl(content, page, source_dsl, target)
130
+ converter = find_page_converter(source_dsl, target)
131
+ if converter
132
+ converter.call(content, page)
133
+ else
134
+ add_warning("No converter for #{source_dsl} → #{target}. Page '#{page[:class_name]}' copied as-is.")
135
+ restructure_page(content)
136
+ end
137
+ end
138
+
139
+ def find_page_converter(source_dsl, target)
140
+ key = :"#{normalize_dsl(source_dsl)}_to_#{target}"
141
+ page_converters[key]
142
+ end
143
+
144
+ def page_converters
145
+ @page_converters ||= {}
146
+ end
147
+
148
+ def register_page_converter(key, converter)
149
+ page_converters[key] = converter
150
+ end
151
+
152
+ # --- Test conversion ---
153
+
154
+ def convert_test(content, test)
155
+ source_framework = test[:type]
156
+ target = @params[:target_framework]
157
+
158
+ if source_framework.to_s == target
159
+ restructure_test(content)
160
+ else
161
+ convert_test_framework(content, test, source_framework, target)
162
+ end
163
+ end
164
+
165
+ # :reek:ControlParameter { enabled: false }
166
+ def convert_test_framework(content, test, source_framework, target)
167
+ converter = find_test_converter(source_framework, target)
168
+ if converter
169
+ converter.call(content, test)
170
+ else
171
+ add_warning("No converter for #{source_framework} → #{target}. Test '#{test[:path]}' copied as-is.")
172
+ restructure_test(content)
173
+ end
174
+ end
175
+
176
+ def find_test_converter(source_framework, target)
177
+ key = :"#{source_framework}_to_#{target}"
178
+ test_converters[key]
179
+ end
180
+
181
+ def test_converters
182
+ @test_converters ||= {}
183
+ end
184
+
185
+ # --- Restructuring (identity conversion) ---
186
+
187
+ def identity_converter
188
+ @identity_converter ||= Converters::IdentityConverter.new(@params[:target_automation])
189
+ end
190
+
191
+ def restructure_page(content)
192
+ identity_converter.convert_page(content, {})
193
+ end
194
+
195
+ def restructure_test(content)
196
+ identity_converter.convert_test(content, {})
197
+ end
198
+
199
+ def convert_step_page_references(content)
200
+ identity_converter.convert_step(content)
201
+ end
202
+
203
+ # --- Path helpers ---
204
+
205
+ def raider_page_path(class_name)
206
+ filename = class_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
207
+ filename = filename.delete_suffix('_page')
208
+ "#{@params[:output_path]}/page_objects/pages/#{filename}.rb"
209
+ end
210
+
211
+ def raider_test_path(test)
212
+ basename = File.basename(test[:path], '.rb')
213
+ target = @params[:target_framework]
214
+
215
+ case target
216
+ when 'rspec'
217
+ name = basename.delete_prefix('test_').delete_suffix('_spec')
218
+ "#{@params[:output_path]}/spec/#{name}_spec.rb"
219
+ when 'minitest'
220
+ name = basename.delete_prefix('test_').delete_suffix('_test').delete_suffix('_spec')
221
+ "#{@params[:output_path]}/test/test_#{name}.rb"
222
+ else
223
+ "#{@params[:output_path]}/spec/#{basename}.rb"
224
+ end
225
+ end
226
+
227
+ def raider_feature_path(source_path)
228
+ filename = File.basename(source_path)
229
+ "#{@params[:output_path]}/features/#{filename}"
230
+ end
231
+
232
+ def raider_step_path(source_path)
233
+ filename = File.basename(source_path)
234
+ "#{@params[:output_path]}/features/step_definitions/#{filename}"
235
+ end
236
+
237
+ # --- Config ---
238
+
239
+ def extract_config_overrides
240
+ overrides = {}
241
+ overrides[:browser] = @params[:browser] || @analysis[:browser]
242
+ overrides[:url] = @params[:url] || @analysis[:url]
243
+ overrides.compact
244
+ end
245
+
246
+ # --- Helpers ---
247
+
248
+ def same_automation_dsl?(source_dsl, target)
249
+ normalize_dsl(source_dsl) == target
250
+ end
251
+
252
+ def normalize_dsl(dsl)
253
+ case dsl
254
+ when :site_prism then 'capybara'
255
+ else dsl.to_s
256
+ end
257
+ end
258
+
259
+ def target_cucumber?
260
+ @params[:target_framework] == 'cucumber'
261
+ end
262
+
263
+ def page_conversion_notes
264
+ source = @analysis[:source_dsl]
265
+ target = @params[:target_automation]
266
+ same_automation_dsl?(source, target) ? 'Restructured to Raider conventions' : "Converted from #{source} to #{target}"
267
+ end
268
+
269
+ def test_conversion_notes(source_type)
270
+ target = @params[:target_framework]
271
+ source_type.to_s == target ? 'Restructured to Raider conventions' : "Converted from #{source_type} to #{target}"
272
+ end
273
+
274
+ def add_warning(message)
275
+ @warnings << message
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'project_detector'
4
+
5
+ module Adopter
6
+ class MobileProjectError < StandardError; end
7
+
8
+ # :reek:TooManyMethods { enabled: false }
9
+ class ProjectAnalyzer
10
+ KNOWN_FRAMEWORK_GEMS = %w[
11
+ activesupport allure-cucumber allure-rspec allure-minitest allure-ruby-commons
12
+ appium_lib appium_console axe-core-rspec axe-core-selenium capybara
13
+ cucumber eyes_selenium eyes_universal minitest minitest-reporters
14
+ parallel_split_test parallel_tests rake reek rspec rubocop rubocop-rspec
15
+ rubocop-minitest ruby_raider selenium-webdriver site_prism watir
16
+ ].freeze
17
+
18
+ PAGE_DSL_PATTERNS = {
19
+ site_prism: /class\s+\w+\s*<\s*SitePrism::Page/,
20
+ capybara: /include\s+Capybara::DSL|\.fill_in\b|\.click_on\b|\.click_button\b/,
21
+ selenium: /\.find_element\s*\(|driver\.navigate/,
22
+ watir: /browser\.text_field|browser\.button|browser\.element/
23
+ }.freeze
24
+
25
+ PAGE_CLASS_PATTERNS = [
26
+ /class\s+\w+\s*<\s*SitePrism::Page/,
27
+ /class\s+\w+\s*<\s*(?:Page|BasePage|AbstractPage)/,
28
+ /include\s+Capybara::DSL/,
29
+ /\.find_element\s*\(/,
30
+ /browser\.text_field|browser\.button/,
31
+ /def\s+(?:visit|login|click_|fill_|select_|navigate)/
32
+ ].freeze
33
+
34
+ TEST_FILE_PATTERNS = {
35
+ rspec: { glob: '**/*_spec.rb', marker: /\b(?:describe|context|it)\b/ },
36
+ cucumber_feature: { glob: '**/*.feature', marker: /\b(?:Feature|Scenario)\b/ },
37
+ cucumber_step: { glob: '**/*_steps.rb', marker: /\b(?:Given|When|Then)\b/ },
38
+ minitest: { glob: '**/test_*.rb', marker: /class\s+\w+\s*<\s*Minitest::Test/ },
39
+ minitest_alt: { glob: '**/*_test.rb', marker: /class\s+\w+\s*<\s*Minitest::Test/ }
40
+ }.freeze
41
+
42
+ HELPER_ROLES = {
43
+ driver: /module\s+DriverHelper|def\s+(?:driver|create_driver)\b/,
44
+ browser: /module\s+BrowserHelper|def\s+(?:browser|create_browser)\b/,
45
+ capybara: /Capybara\.configure|Capybara\.register_driver/,
46
+ env: /^(?:Before|After)\s+do\b/,
47
+ factory: /FactoryBot|ModelFactory|def\s+self\.for\b/,
48
+ spec_helper: /RSpec\.configure|Minitest::Test/
49
+ }.freeze
50
+
51
+ def initialize(source_path, overrides = {})
52
+ @source_path = source_path
53
+ @overrides = overrides
54
+ end
55
+
56
+ def analyze
57
+ detection = ProjectDetector.detect(@source_path)
58
+ validate_web_only!(detection)
59
+
60
+ {
61
+ **detection,
62
+ **@overrides.compact,
63
+ pages: discover_pages,
64
+ tests: discover_tests,
65
+ helpers: discover_helpers,
66
+ features: discover_features,
67
+ step_definitions: discover_step_definitions,
68
+ custom_gems: discover_custom_gems,
69
+ source_dsl: detect_page_dsl
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def validate_web_only!(detection)
76
+ return unless detection[:automation] == 'appium'
77
+
78
+ raise MobileProjectError,
79
+ 'Mobile (Appium) projects cannot be adopted. Only web-based projects are supported.'
80
+ end
81
+
82
+ # :reek:FeatureEnvy { enabled: false }
83
+ def discover_pages
84
+ page_path = detect_page_dir
85
+ return [] unless page_path
86
+
87
+ ruby_files_in(page_path).filter_map do |file|
88
+ content = File.read(file)
89
+ next unless page_like?(content)
90
+
91
+ {
92
+ path: relative_path(file),
93
+ class_name: extract_class_name(content),
94
+ base_class: extract_base_class(content),
95
+ methods: extract_public_methods(content)
96
+ }
97
+ end
98
+ end
99
+
100
+ def discover_tests
101
+ results = []
102
+ TEST_FILE_PATTERNS.each do |type, config|
103
+ Dir.glob(File.join(@source_path, config[:glob])).each do |file|
104
+ content = File.read(file)
105
+ next unless content.match?(config[:marker])
106
+
107
+ results << {
108
+ path: relative_path(file),
109
+ type: type.to_s.split('_').first.to_sym,
110
+ class_name: extract_class_name(content),
111
+ test_methods: extract_test_methods(content, type)
112
+ }
113
+ end
114
+ end
115
+ results
116
+ end
117
+
118
+ # :reek:FeatureEnvy { enabled: false }
119
+ def discover_helpers
120
+ helper_files = Dir.glob(File.join(@source_path, '**', '*helper*.rb')) +
121
+ Dir.glob(File.join(@source_path, '**', 'support', '*.rb')) +
122
+ Dir.glob(File.join(@source_path, '**', 'env.rb'))
123
+
124
+ helper_files.uniq.first(30).filter_map do |file|
125
+ next unless File.file?(file)
126
+
127
+ content = File.read(file)
128
+ role = detect_helper_role(content)
129
+
130
+ {
131
+ path: relative_path(file),
132
+ role: role,
133
+ modules_defined: extract_modules(content)
134
+ }
135
+ end
136
+ end
137
+
138
+ def discover_features
139
+ Dir.glob(File.join(@source_path, '**', '*.feature')).map do |file|
140
+ {
141
+ path: relative_path(file),
142
+ scenarios: count_scenarios(File.read(file))
143
+ }
144
+ end
145
+ end
146
+
147
+ def discover_step_definitions
148
+ Dir.glob(File.join(@source_path, '**', '*_steps.rb')).map do |file|
149
+ {
150
+ path: relative_path(file),
151
+ steps: count_steps(File.read(file))
152
+ }
153
+ end
154
+ end
155
+
156
+ # :reek:NestedIterators { enabled: false }
157
+ def discover_custom_gems
158
+ gemfile = File.join(@source_path, 'Gemfile')
159
+ return [] unless File.exist?(gemfile)
160
+
161
+ ProjectDetector.send(:parse_gemfile, @source_path).reject do |gem_name|
162
+ KNOWN_FRAMEWORK_GEMS.include?(gem_name)
163
+ end
164
+ end
165
+
166
+ def detect_page_dsl
167
+ page_files = discover_pages.map { |p| File.join(@source_path, p[:path]) }
168
+ return :raw if page_files.empty?
169
+
170
+ page_contents = page_files.first(20).map { |f| File.read(f) }.join("\n")
171
+
172
+ PAGE_DSL_PATTERNS.each do |dsl, pattern|
173
+ return dsl if page_contents.match?(pattern)
174
+ end
175
+
176
+ :raw
177
+ end
178
+
179
+ # --- File discovery helpers ---
180
+
181
+ def detect_page_dir
182
+ detected = ProjectDetector.detect_page_path(@source_path)
183
+ return File.join(@source_path, detected) if detected
184
+
185
+ # Fallback: scan all .rb files for page-like classes
186
+ nil
187
+ end
188
+
189
+ def ruby_files_in(dir)
190
+ Dir.glob(File.join(dir, '**', '*.rb'))
191
+ end
192
+
193
+ def relative_path(absolute)
194
+ absolute.sub("#{@source_path}/", '')
195
+ end
196
+
197
+ # --- Content analysis helpers ---
198
+
199
+ def page_like?(content)
200
+ PAGE_CLASS_PATTERNS.any? { |pattern| content.match?(pattern) }
201
+ end
202
+
203
+ def extract_class_name(content)
204
+ match = content.match(/class\s+(\w+)/)
205
+ match&.[](1)
206
+ end
207
+
208
+ def extract_base_class(content)
209
+ match = content.match(/class\s+\w+\s*<\s*([\w:]+)/)
210
+ match&.[](1)
211
+ end
212
+
213
+ def extract_public_methods(content)
214
+ in_private = false
215
+ content.each_line.filter_map do |line|
216
+ in_private = true if line.match?(/^\s*private\b/)
217
+ next if in_private
218
+
219
+ match = line.match(/^\s*def\s+(\w+)/)
220
+ match[1] if match && match[1] != 'initialize'
221
+ end
222
+ end
223
+
224
+ def extract_test_methods(content, type)
225
+ case type
226
+ when :rspec
227
+ content.scan(/\bit\s+['"]([^'"]+)['"]/).flatten
228
+ when :minitest, :minitest_alt
229
+ content.scan(/def\s+(test_\w+)/).flatten
230
+ when :cucumber_step
231
+ content.scan(/(?:Given|When|Then)\(['"]([^'"]+)['"]/).flatten
232
+ else
233
+ []
234
+ end
235
+ end
236
+
237
+ def extract_modules(content)
238
+ content.scan(/module\s+(\w+)/).flatten
239
+ end
240
+
241
+ def detect_helper_role(content)
242
+ HELPER_ROLES.each do |role, pattern|
243
+ return role if content.match?(pattern)
244
+ end
245
+ :custom
246
+ end
247
+
248
+ def count_scenarios(content)
249
+ content.scan(/^\s*Scenario/).length
250
+ end
251
+
252
+ def count_steps(content)
253
+ content.scan(/^\s*(?:Given|When|Then|And|But)\b/).length
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Adopter
4
+ # :reek:TooManyMethods { enabled: false }
5
+ module ProjectDetector
6
+ GEM_AUTOMATION_MAP = {
7
+ 'site_prism' => 'capybara',
8
+ 'capybara' => 'capybara',
9
+ 'selenium-webdriver' => 'selenium',
10
+ 'watir' => 'watir',
11
+ 'appium_lib' => 'appium',
12
+ 'eyes_selenium' => 'applitools',
13
+ 'axe-core-selenium' => 'axe',
14
+ 'axe-core-rspec' => 'axe'
15
+ }.freeze
16
+
17
+ GEM_FRAMEWORK_MAP = {
18
+ 'cucumber' => 'cucumber',
19
+ 'rspec' => 'rspec',
20
+ 'minitest' => 'minitest'
21
+ }.freeze
22
+
23
+ BROWSERS = %w[chrome firefox safari edge].freeze
24
+
25
+ module_function
26
+
27
+ def detect(path = '.')
28
+ {
29
+ automation: detect_automation(path),
30
+ framework: detect_framework(path),
31
+ page_path: detect_page_path(path),
32
+ spec_path: detect_spec_path(path),
33
+ feature_path: detect_feature_path(path),
34
+ helper_path: detect_helper_path(path),
35
+ browser: detect_browser(path),
36
+ url: detect_url(path),
37
+ ci_platform: detect_ci_platform(path)
38
+ }.compact
39
+ end
40
+
41
+ def detect_automation(path)
42
+ gems = parse_gemfile(path)
43
+ GEM_AUTOMATION_MAP.each do |gem_name, automation|
44
+ return automation if gems.include?(gem_name)
45
+ end
46
+ detect_automation_from_requires(path)
47
+ end
48
+
49
+ def detect_framework(path)
50
+ gems = parse_gemfile(path)
51
+ GEM_FRAMEWORK_MAP.each do |gem_name, framework|
52
+ return framework if gems.include?(gem_name)
53
+ end
54
+ return 'rspec' if Dir.exist?(File.join(path, 'spec'))
55
+ return 'cucumber' if Dir.exist?(File.join(path, 'features'))
56
+ return 'minitest' if Dir.exist?(File.join(path, 'test'))
57
+
58
+ nil
59
+ end
60
+
61
+ def detect_page_path(path)
62
+ candidates = %w[page_objects/pages page_objects pages page]
63
+ find_existing_dir(path, candidates)
64
+ end
65
+
66
+ def detect_spec_path(path)
67
+ candidates = %w[spec spec/features spec/tests test tests]
68
+ find_existing_dir(path, candidates)
69
+ end
70
+
71
+ def detect_feature_path(path)
72
+ candidates = %w[features features/scenarios]
73
+ find_existing_dir(path, candidates)
74
+ end
75
+
76
+ def detect_helper_path(path)
77
+ candidates = %w[helpers support spec/support features/support]
78
+ find_existing_dir(path, candidates)
79
+ end
80
+
81
+ # :reek:NestedIterators { enabled: false }
82
+ def detect_browser(path)
83
+ config_files = helper_and_config_files(path)
84
+ config_files.each do |file|
85
+ next unless File.exist?(file)
86
+
87
+ content = File.read(file)
88
+ BROWSERS.each do |browser|
89
+ return browser if content.match?(/(?:browser|driver)\s*[:=]\s*[:'"]?#{browser}\b/i)
90
+ end
91
+ end
92
+ nil
93
+ end
94
+
95
+ def detect_url(path)
96
+ config_files = helper_and_config_files(path)
97
+ config_files.each do |file|
98
+ next unless File.exist?(file)
99
+
100
+ content = File.read(file)
101
+ match = content.match(%r{(?:base_url|url|app_host)\s*[:=]\s*['"]?(https?://[^\s'"]+)})
102
+ return match[1] if match
103
+ end
104
+ nil
105
+ end
106
+
107
+ def detect_ci_platform(path)
108
+ return 'github' if Dir.exist?(File.join(path, '.github', 'workflows'))
109
+ return 'gitlab' if File.exist?(File.join(path, '.gitlab-ci.yml'))
110
+
111
+ nil
112
+ end
113
+
114
+ def parse_gemfile(path)
115
+ gemfile = File.join(path, 'Gemfile')
116
+ return [] unless File.exist?(gemfile)
117
+
118
+ File.readlines(gemfile).filter_map do |line|
119
+ match = line.match(/^\s*gem\s+['"]([^'"]+)['"]/)
120
+ match[1] if match
121
+ end
122
+ end
123
+
124
+ def detect_automation_from_requires(path)
125
+ ruby_files = Dir.glob(File.join(path, '**', '*.rb'))
126
+ ruby_files.first(50).each do |file|
127
+ content = File.read(file)
128
+ return 'capybara' if content.include?("require 'capybara'") || content.include?("require 'site_prism'")
129
+ return 'selenium' if content.include?("require 'selenium-webdriver'")
130
+ return 'watir' if content.include?("require 'watir'")
131
+ return 'appium' if content.include?("require 'appium_lib'")
132
+ end
133
+ nil
134
+ end
135
+
136
+ def find_existing_dir(path, candidates)
137
+ candidates.each do |candidate|
138
+ return candidate if Dir.exist?(File.join(path, candidate))
139
+ end
140
+ nil
141
+ end
142
+
143
+ def helper_and_config_files(path)
144
+ explicit = [
145
+ File.join(path, 'config', 'config.yml'),
146
+ File.join(path, 'spec', 'spec_helper.rb'),
147
+ File.join(path, 'test', 'test_helper.rb'),
148
+ File.join(path, 'features', 'support', 'env.rb'),
149
+ File.join(path, 'support', 'env.rb'),
150
+ File.join(path, '.env')
151
+ ]
152
+ helpers = Dir.glob(File.join(path, '**', '*helper*.rb')).first(10)
153
+ explicit + helpers
154
+ end
155
+
156
+ private_class_method :detect_automation_from_requires, :find_existing_dir,
157
+ :helper_and_config_files, :parse_gemfile
158
+ end
159
+ end