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.
- checksums.yaml +4 -4
- data/.github/workflows/integration.yml +4 -6
- data/.github/workflows/reek.yml +6 -5
- data/.github/workflows/release.yml +175 -0
- data/.github/workflows/rubocop.yml +7 -6
- data/.github/workflows/system_tests.yml +83 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +24 -0
- data/README.md +3 -1
- data/RELEASE.md +412 -0
- data/RELEASE_QUICK_GUIDE.md +77 -0
- data/bin/release +186 -0
- data/lib/adopter/adopt_menu.rb +150 -0
- data/lib/adopter/converters/base_converter.rb +85 -0
- data/lib/adopter/converters/identity_converter.rb +56 -0
- data/lib/adopter/migration_plan.rb +75 -0
- data/lib/adopter/migrator.rb +96 -0
- data/lib/adopter/plan_builder.rb +278 -0
- data/lib/adopter/project_analyzer.rb +256 -0
- data/lib/adopter/project_detector.rb +159 -0
- data/lib/commands/adopt_commands.rb +43 -0
- data/lib/generators/automation/templates/account.tt +9 -5
- data/lib/generators/automation/templates/appium_caps.tt +60 -6
- data/lib/generators/automation/templates/home.tt +4 -4
- data/lib/generators/automation/templates/login.tt +61 -4
- data/lib/generators/automation/templates/page.tt +13 -7
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -4
- data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -1
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -4
- data/lib/generators/automation/templates/partials/visit_method.tt +11 -1
- data/lib/generators/automation/templates/pdp.tt +1 -1
- data/lib/generators/cucumber/templates/env.tt +6 -4
- data/lib/generators/cucumber/templates/partials/capybara_env.tt +20 -0
- data/lib/generators/cucumber/templates/partials/capybara_world.tt +6 -0
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +2 -2
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -3
- data/lib/generators/cucumber/templates/steps.tt +2 -2
- data/lib/generators/cucumber/templates/world.tt +5 -3
- data/lib/generators/generator.rb +14 -2
- data/lib/generators/helper_generator.rb +16 -3
- data/lib/generators/infrastructure/github_generator.rb +6 -0
- data/lib/generators/infrastructure/templates/github.tt +11 -7
- data/lib/generators/infrastructure/templates/github_appium.tt +108 -0
- data/lib/generators/infrastructure/templates/gitlab.tt +5 -2
- data/lib/generators/invoke_generators.rb +1 -0
- data/lib/generators/menu_generator.rb +2 -0
- data/lib/generators/minitest/minitest_generator.rb +23 -0
- data/lib/generators/minitest/templates/test.tt +93 -0
- data/lib/generators/rspec/templates/spec.tt +12 -10
- data/lib/generators/template_renderer/partial_cache.rb +116 -0
- data/lib/generators/template_renderer/partial_resolver.rb +103 -0
- data/lib/generators/template_renderer/template_error.rb +50 -0
- data/lib/generators/template_renderer.rb +90 -0
- data/lib/generators/templates/common/config.tt +2 -2
- data/lib/generators/templates/common/gemfile.tt +15 -3
- data/lib/generators/templates/common/partials/web_config.tt +1 -1
- data/lib/generators/templates/common/read_me.tt +3 -1
- data/lib/generators/templates/helpers/allure_helper.tt +2 -2
- data/lib/generators/templates/helpers/browser_helper.tt +1 -0
- data/lib/generators/templates/helpers/capybara_helper.tt +28 -0
- data/lib/generators/templates/helpers/driver_helper.tt +1 -1
- data/lib/generators/templates/helpers/partials/allure_imports.tt +3 -1
- data/lib/generators/templates/helpers/partials/allure_requirements.tt +3 -1
- data/lib/generators/templates/helpers/partials/appium_driver.tt +46 -0
- data/lib/generators/templates/helpers/partials/axe_driver.tt +10 -0
- data/lib/generators/templates/helpers/partials/browserstack_config.tt +13 -0
- data/lib/generators/templates/helpers/partials/driver_and_options.tt +6 -114
- data/lib/generators/templates/helpers/partials/quit_driver.tt +3 -1
- data/lib/generators/templates/helpers/partials/screenshot.tt +3 -1
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +25 -0
- data/lib/generators/templates/helpers/spec_helper.tt +17 -4
- data/lib/generators/templates/helpers/test_helper.tt +26 -0
- data/lib/generators/templates/helpers/visual_spec_helper.tt +1 -1
- data/lib/ruby_raider.rb +5 -0
- data/lib/version +1 -1
- data/spec/adopter/adopt_menu_spec.rb +176 -0
- data/spec/adopter/converters/identity_converter_spec.rb +145 -0
- data/spec/adopter/migration_plan_spec.rb +113 -0
- data/spec/adopter/migrator_spec.rb +277 -0
- data/spec/adopter/plan_builder_spec.rb +298 -0
- data/spec/adopter/project_analyzer_spec.rb +337 -0
- data/spec/adopter/project_detector_spec.rb +295 -0
- data/spec/generators/fixtures/templates/test.tt +1 -0
- data/spec/generators/fixtures/templates/test_partial.tt +1 -0
- data/spec/generators/template_renderer_spec.rb +298 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +2 -2
- data/spec/integration/commands/utility_commands_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +325 -0
- data/spec/integration/generators/automation_generator_spec.rb +11 -11
- data/spec/integration/generators/common_generator_spec.rb +40 -40
- data/spec/integration/generators/cucumber_generator_spec.rb +7 -7
- data/spec/integration/generators/github_generator_spec.rb +8 -8
- data/spec/integration/generators/gitlab_generator_spec.rb +8 -8
- data/spec/integration/generators/helpers_generator_spec.rb +73 -35
- data/spec/integration/generators/minitest_generator_spec.rb +70 -0
- data/spec/integration/generators/rspec_generator_spec.rb +7 -7
- data/spec/integration/settings_helper.rb +1 -1
- data/spec/integration/spec_helper.rb +20 -2
- data/spec/system/capybara_spec.rb +42 -0
- data/spec/system/selenium_spec.rb +19 -17
- data/spec/system/support/system_test_helper.rb +35 -0
- data/spec/system/watir_spec.rb +19 -17
- metadata +46 -16
- data/.github/workflows/push_gem.yml +0 -37
- data/.github/workflows/selenium.yml +0 -22
- data/.github/workflows/watir.yml +0 -22
- data/lib/generators/automation/templates/partials/android_caps.tt +0 -17
- data/lib/generators/automation/templates/partials/cross_platform_caps.tt +0 -25
- data/lib/generators/automation/templates/partials/ios_caps.tt +0 -18
- data/lib/generators/automation/templates/partials/selenium_account.tt +0 -9
- data/lib/generators/automation/templates/partials/selenium_login.tt +0 -34
- data/lib/generators/automation/templates/partials/watir_account.tt +0 -7
- 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
|