ruby_raider 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +58 -0
  3. data/.github/workflows/steep.yml +21 -0
  4. data/.gitignore +1 -1
  5. data/.reek.yml +46 -4
  6. data/.ruby-version +1 -1
  7. data/README.md +138 -77
  8. data/Steepfile +22 -0
  9. data/assets/ruby_raider_logo.svg +51 -0
  10. data/lib/adopter/adopt_menu.rb +11 -15
  11. data/lib/adopter/converters/base_converter.rb +1 -2
  12. data/lib/adopter/converters/identity_converter.rb +3 -6
  13. data/lib/adopter/migration_plan.rb +0 -1
  14. data/lib/adopter/plan_builder.rb +2 -5
  15. data/lib/adopter/project_analyzer.rb +1 -5
  16. data/lib/adopter/project_detector.rb +3 -5
  17. data/lib/commands/adopt_commands.rb +0 -1
  18. data/lib/commands/plugin_commands.rb +0 -2
  19. data/lib/commands/scaffolding_commands.rb +220 -37
  20. data/lib/commands/utility_commands.rb +82 -2
  21. data/lib/generators/automation/automation_generator.rb +0 -7
  22. data/lib/generators/automation/templates/partials/element.tt +1 -1
  23. data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -7
  24. data/lib/generators/automation/templates/partials/url_methods.tt +0 -1
  25. data/lib/generators/common_generator.rb +12 -0
  26. data/lib/generators/cucumber/cucumber_generator.rb +36 -0
  27. data/lib/generators/cucumber/templates/accessibility_feature.tt +5 -0
  28. data/lib/generators/cucumber/templates/accessibility_steps.tt +21 -0
  29. data/lib/generators/cucumber/templates/cucumber.tt +8 -1
  30. data/lib/generators/cucumber/templates/feature.tt +0 -4
  31. data/lib/generators/cucumber/templates/partials/appium_env.tt +5 -0
  32. data/lib/generators/cucumber/templates/partials/capybara_env.tt +19 -1
  33. data/lib/generators/cucumber/templates/partials/driver_world.tt +1 -4
  34. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -35
  35. data/lib/generators/cucumber/templates/partials/watir_env.tt +20 -1
  36. data/lib/generators/cucumber/templates/partials/web_steps.tt +6 -12
  37. data/lib/generators/cucumber/templates/performance_feature.tt +5 -0
  38. data/lib/generators/cucumber/templates/performance_steps.tt +17 -0
  39. data/lib/generators/cucumber/templates/visual_feature.tt +5 -0
  40. data/lib/generators/cucumber/templates/visual_steps.tt +19 -0
  41. data/lib/generators/generator.rb +38 -7
  42. data/lib/generators/helper_generator.rb +24 -7
  43. data/lib/generators/infrastructure/templates/github.tt +1 -1
  44. data/lib/generators/infrastructure/templates/github_appium.tt +2 -2
  45. data/lib/generators/infrastructure/templates/gitlab.tt +1 -1
  46. data/lib/generators/invoke_generators.rb +42 -9
  47. data/lib/generators/menu_generator.rb +120 -11
  48. data/lib/generators/minitest/minitest_generator.rb +16 -4
  49. data/lib/generators/minitest/templates/accessibility_test.tt +26 -0
  50. data/lib/generators/minitest/templates/performance_test.tt +18 -0
  51. data/lib/generators/minitest/templates/test.tt +5 -34
  52. data/lib/generators/minitest/templates/visual_test.tt +23 -0
  53. data/lib/generators/rspec/rspec_generator.rb +16 -4
  54. data/lib/generators/rspec/templates/accessibility_spec.tt +25 -0
  55. data/lib/generators/rspec/templates/performance_spec.tt +18 -0
  56. data/lib/generators/rspec/templates/spec.tt +5 -35
  57. data/lib/generators/rspec/templates/visual_spec.tt +20 -0
  58. data/lib/generators/template_renderer/partial_cache.rb +11 -1
  59. data/lib/generators/template_renderer/partial_resolver.rb +17 -10
  60. data/lib/generators/template_renderer.rb +17 -1
  61. data/lib/generators/templates/common/gemfile.tt +21 -6
  62. data/lib/generators/templates/common/git_ignore.tt +6 -1
  63. data/lib/generators/templates/common/partials/mobile_config.tt +5 -1
  64. data/lib/generators/templates/common/partials/web_config.tt +16 -7
  65. data/lib/generators/templates/common/rakefile.tt +36 -0
  66. data/lib/generators/templates/common/read_me.tt +41 -91
  67. data/lib/generators/templates/common/rspec.tt +3 -0
  68. data/lib/generators/templates/common/ruby_version.tt +1 -0
  69. data/lib/generators/templates/helpers/allure_helper.tt +11 -0
  70. data/lib/generators/templates/helpers/browser_helper.tt +12 -2
  71. data/lib/generators/templates/helpers/capybara_helper.tt +5 -1
  72. data/lib/generators/templates/helpers/debug_helper.tt +190 -0
  73. data/lib/generators/templates/helpers/driver_helper.tt +2 -10
  74. data/lib/generators/templates/helpers/partials/appium_driver.tt +0 -2
  75. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +7 -0
  76. data/lib/generators/templates/helpers/partials/debug_start.tt +7 -0
  77. data/lib/generators/templates/helpers/partials/driver_and_options.tt +1 -3
  78. data/lib/generators/templates/helpers/partials/selenium_driver.tt +8 -7
  79. data/lib/generators/templates/helpers/partials/video_start.tt +9 -0
  80. data/lib/generators/templates/helpers/partials/video_stop.tt +4 -0
  81. data/lib/generators/templates/helpers/performance_helper.tt +57 -0
  82. data/lib/generators/templates/helpers/spec_helper.tt +57 -8
  83. data/lib/generators/templates/helpers/test_helper.tt +69 -1
  84. data/lib/generators/templates/helpers/video_helper.tt +270 -0
  85. data/lib/generators/templates/helpers/visual_helper.tt +39 -46
  86. data/lib/llm/client.rb +79 -0
  87. data/lib/llm/config.rb +57 -0
  88. data/lib/llm/prompts.rb +84 -0
  89. data/lib/llm/provider.rb +27 -0
  90. data/lib/llm/providers/anthropic_provider.rb +43 -0
  91. data/lib/llm/providers/ollama_provider.rb +56 -0
  92. data/lib/llm/providers/openai_provider.rb +42 -0
  93. data/lib/llm/response_parser.rb +67 -0
  94. data/lib/plugin/plugin.rb +22 -20
  95. data/lib/plugin/plugin_exposer.rb +16 -38
  96. data/lib/ruby_raider.rb +47 -12
  97. data/lib/scaffolding/crud_generator.rb +94 -0
  98. data/lib/scaffolding/dry_run_presenter.rb +16 -0
  99. data/lib/scaffolding/name_normalizer.rb +63 -0
  100. data/lib/scaffolding/page_introspector.rb +45 -0
  101. data/lib/scaffolding/project_detector.rb +72 -0
  102. data/lib/scaffolding/scaffold_menu.rb +103 -0
  103. data/lib/scaffolding/scaffolding.rb +158 -11
  104. data/lib/scaffolding/templates/component.tt +30 -0
  105. data/lib/scaffolding/templates/feature.tt +4 -4
  106. data/lib/scaffolding/templates/helper.tt +15 -1
  107. data/lib/scaffolding/templates/page_from_url.tt +75 -0
  108. data/lib/scaffolding/templates/page_object.tt +50 -1
  109. data/lib/scaffolding/templates/spec.tt +33 -2
  110. data/lib/scaffolding/templates/spec_from_page.tt +31 -0
  111. data/lib/scaffolding/templates/spec_from_url.tt +46 -0
  112. data/lib/scaffolding/templates/steps.tt +17 -5
  113. data/lib/scaffolding/url_analyzer.rb +179 -0
  114. data/lib/utilities/desktop_downloader.rb +177 -0
  115. data/lib/utilities/logo.rb +83 -0
  116. data/lib/utilities/utilities.rb +53 -20
  117. data/lib/version +1 -1
  118. data/ruby_raider.gemspec +1 -0
  119. data/sig/adopter/adopt_menu.rbs +25 -0
  120. data/sig/adopter/converters/base_converter.rbs +23 -0
  121. data/sig/adopter/converters/identity_converter.rbs +16 -0
  122. data/sig/adopter/migration_plan.rbs +34 -0
  123. data/sig/adopter/migrator.rbs +21 -0
  124. data/sig/adopter/plan_builder.rbs +38 -0
  125. data/sig/adopter/project_analyzer.rbs +39 -0
  126. data/sig/adopter/project_detector.rbs +26 -0
  127. data/sig/commands/adopt_commands.rbs +8 -0
  128. data/sig/commands/loaded_commands.rbs +5 -0
  129. data/sig/commands/plugin_commands.rbs +9 -0
  130. data/sig/commands/scaffolding_commands.rbs +28 -0
  131. data/sig/commands/utility_commands.rbs +21 -0
  132. data/sig/generators/automation/automation_generator.rbs +20 -0
  133. data/sig/generators/common_generator.rbs +12 -0
  134. data/sig/generators/cucumber/cucumber_generator.rbs +16 -0
  135. data/sig/generators/generator.rbs +40 -0
  136. data/sig/generators/helper_generator.rbs +18 -0
  137. data/sig/generators/infrastructure/github_generator.rbs +5 -0
  138. data/sig/generators/infrastructure/gitlab_generator.rbs +4 -0
  139. data/sig/generators/invoke_generators.rbs +10 -0
  140. data/sig/generators/menu_generator.rbs +29 -0
  141. data/sig/generators/minitest/minitest_generator.rbs +8 -0
  142. data/sig/generators/rspec/rspec_generator.rbs +8 -0
  143. data/sig/generators/template_renderer/partial_cache.rbs +20 -0
  144. data/sig/generators/template_renderer/partial_resolver.rbs +20 -0
  145. data/sig/generators/template_renderer/template_error.rbs +19 -0
  146. data/sig/generators/template_renderer.rbs +10 -0
  147. data/sig/llm/client.rbs +15 -0
  148. data/sig/llm/config.rbs +20 -0
  149. data/sig/llm/prompts.rbs +8 -0
  150. data/sig/llm/provider.rbs +12 -0
  151. data/sig/llm/providers/anthropic_provider.rbs +16 -0
  152. data/sig/llm/providers/ollama_provider.rbs +18 -0
  153. data/sig/llm/providers/openai_provider.rbs +16 -0
  154. data/sig/llm/response_parser.rbs +13 -0
  155. data/sig/plugin/plugin.rbs +24 -0
  156. data/sig/plugin/plugin_exposer.rbs +20 -0
  157. data/sig/ruby_raider.rbs +15 -0
  158. data/sig/scaffolding/crud_generator.rbs +16 -0
  159. data/sig/scaffolding/dry_run_presenter.rbs +4 -0
  160. data/sig/scaffolding/name_normalizer.rbs +17 -0
  161. data/sig/scaffolding/page_introspector.rbs +14 -0
  162. data/sig/scaffolding/project_detector.rbs +14 -0
  163. data/sig/scaffolding/scaffold_menu.rbs +18 -0
  164. data/sig/scaffolding/scaffolding.rbs +55 -0
  165. data/sig/scaffolding/url_analyzer.rbs +28 -0
  166. data/sig/utilities/desktop_downloader.rbs +23 -0
  167. data/sig/utilities/logger.rbs +13 -0
  168. data/sig/utilities/logo.rbs +16 -0
  169. data/sig/utilities/utilities.rbs +30 -0
  170. data/sig/vendor/thor.rbs +34 -0
  171. data/sig/vendor/tty_prompt.rbs +15 -0
  172. data/spec/adopter/adopt_menu_spec.rb +12 -12
  173. data/spec/adopter/migration_plan_spec.rb +1 -1
  174. data/spec/adopter/migrator_spec.rb +2 -2
  175. data/spec/adopter/project_detector_spec.rb +1 -1
  176. data/spec/commands/raider_commands_spec.rb +129 -0
  177. data/spec/generators/generator_spec.rb +23 -0
  178. data/spec/integration/commands/scaffolding_commands_spec.rb +1 -1
  179. data/spec/integration/commands/utility_commands_spec.rb +23 -3
  180. data/spec/integration/content/ci_content_spec.rb +119 -0
  181. data/spec/integration/content/common_content_spec.rb +288 -0
  182. data/spec/integration/content/config_content_spec.rb +175 -0
  183. data/spec/integration/content/content_helper.rb +32 -0
  184. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  185. data/spec/integration/content/helper_content_spec.rb +485 -0
  186. data/spec/integration/content/page_content_spec.rb +259 -0
  187. data/spec/integration/content/reporter_content_spec.rb +236 -0
  188. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  189. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  190. data/spec/integration/content/test_content_spec.rb +266 -0
  191. data/spec/integration/end_to_end_features_spec.rb +690 -0
  192. data/spec/integration/end_to_end_spec.rb +52 -16
  193. data/spec/integration/generators/automation_generator_spec.rb +0 -12
  194. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  195. data/spec/integration/generators/common_generator_spec.rb +12 -13
  196. data/spec/integration/generators/config_features_spec.rb +155 -0
  197. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  198. data/spec/integration/generators/helpers_generator_spec.rb +0 -12
  199. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  200. data/spec/integration/generators/minitest_generator_spec.rb +0 -6
  201. data/spec/integration/generators/reporter_spec.rb +159 -0
  202. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  203. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  204. data/spec/integration/settings_helper.rb +0 -3
  205. data/spec/integration/spec_helper.rb +30 -13
  206. data/spec/llm/client_spec.rb +79 -0
  207. data/spec/llm/config_spec.rb +92 -0
  208. data/spec/llm/prompts_spec.rb +49 -0
  209. data/spec/llm/response_parser_spec.rb +92 -0
  210. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  211. data/spec/menus/menu_generator_spec.rb +263 -0
  212. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  213. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  214. data/spec/scaffolding/scaffold_project_detector_spec.rb +104 -0
  215. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  216. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  217. data/spec/system/adopt_matrix_spec.rb +537 -0
  218. data/spec/system/adopt_spec.rb +225 -0
  219. data/spec/system/support/system_test_helper.rb +0 -2
  220. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  221. metadata +150 -5
  222. data/lib/generators/automation/templates/visual_options.tt +0 -16
  223. data/lib/generators/templates/helpers/partials/axe_driver.tt +0 -10
  224. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/client'
4
+ require_relative '../../lib/llm/provider'
5
+
6
+ RSpec.describe Llm::Client do
7
+ let(:mock_provider) { instance_double(Llm::Provider, available?: true) }
8
+ let(:mock_config) { instance_double(Llm::Config, configured?: true, provider_name: 'ollama', model: 'llama3.2') }
9
+
10
+ before do
11
+ allow(Llm::Config).to receive(:new).and_return(mock_config)
12
+ allow(mock_config).to receive(:build_provider).and_return(mock_provider)
13
+ end
14
+
15
+ describe '.complete' do
16
+ it 'delegates to the provider' do
17
+ allow(mock_provider).to receive(:complete).and_return('AI response')
18
+ result = described_class.complete('test prompt')
19
+ expect(result).to eq('AI response')
20
+ end
21
+
22
+ it 'returns nil when not configured' do
23
+ allow(mock_config).to receive(:configured?).and_return(false)
24
+ expect(described_class.complete('test prompt')).to be_nil
25
+ end
26
+
27
+ it 'returns nil when provider is not available' do
28
+ allow(mock_provider).to receive(:available?).and_return(false)
29
+ expect(described_class.complete('test prompt')).to be_nil
30
+ end
31
+
32
+ it 'retries on failure and returns nil after max retries' do
33
+ allow(mock_provider).to receive(:complete).and_raise(StandardError, 'network error')
34
+ allow(described_class).to receive(:sleep) # skip actual sleep
35
+ expect(described_class.complete('test prompt')).to be_nil
36
+ end
37
+
38
+ it 'passes system_prompt to provider' do
39
+ allow(mock_provider).to receive(:complete).and_return('ok')
40
+ result = described_class.complete('prompt', system_prompt: 'system')
41
+ expect(result).to eq('ok')
42
+ expect(mock_provider).to have_received(:complete).with('prompt', system_prompt: 'system')
43
+ end
44
+ end
45
+
46
+ describe '.available?' do
47
+ it 'returns true when provider is configured and available' do
48
+ expect(described_class).to be_available
49
+ end
50
+
51
+ it 'returns false when not configured' do
52
+ allow(mock_config).to receive(:configured?).and_return(false)
53
+ expect(described_class).not_to be_available
54
+ end
55
+
56
+ it 'returns false when provider is not available' do
57
+ allow(mock_provider).to receive(:available?).and_return(false)
58
+ expect(described_class).not_to be_available
59
+ end
60
+ end
61
+
62
+ describe '.status' do
63
+ it 'returns configured status with details' do
64
+ allow(mock_provider).to receive(:available?).and_return(true)
65
+ status = described_class.status
66
+ expect(status[:configured]).to be true
67
+ expect(status[:provider]).to eq('ollama')
68
+ expect(status[:model]).to eq('llama3.2')
69
+ expect(status[:available]).to be true
70
+ end
71
+
72
+ it 'returns unconfigured status' do
73
+ allow(mock_config).to receive(:configured?).and_return(false)
74
+ status = described_class.status
75
+ expect(status[:configured]).to be false
76
+ expect(status[:provider]).to be_nil
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/config'
4
+
5
+ RSpec.describe Llm::Config do
6
+ before do
7
+ allow(ENV).to receive(:fetch).and_call_original
8
+ %w[RUBY_RAIDER_LLM_PROVIDER RUBY_RAIDER_LLM_API_KEY RUBY_RAIDER_LLM_MODEL RUBY_RAIDER_LLM_URL].each do |key|
9
+ allow(ENV).to receive(:fetch).with(key, nil).and_return(nil)
10
+ end
11
+ allow(File).to receive(:exist?).with('config/config.yml').and_return(false)
12
+ end
13
+
14
+ describe '#configured?' do
15
+ it 'returns false when no provider is set' do
16
+ expect(described_class.new).not_to be_configured
17
+ end
18
+
19
+ it 'returns false for unknown provider' do
20
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('unknown')
21
+ expect(described_class.new).not_to be_configured
22
+ end
23
+
24
+ it 'returns true for ollama without api key' do
25
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('ollama')
26
+ expect(described_class.new).to be_configured
27
+ end
28
+
29
+ it 'returns false for openai without api key' do
30
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('openai')
31
+ expect(described_class.new).not_to be_configured
32
+ end
33
+
34
+ it 'returns true for openai with api key' do
35
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('openai')
36
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-test')
37
+ expect(described_class.new).to be_configured
38
+ end
39
+
40
+ it 'returns true for anthropic with api key' do
41
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('anthropic')
42
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-ant-test')
43
+ expect(described_class.new).to be_configured
44
+ end
45
+ end
46
+
47
+ describe '#build_provider' do
48
+ it 'returns nil when not configured' do
49
+ expect(described_class.new.build_provider).to be_nil
50
+ end
51
+
52
+ it 'builds OpenaiProvider for openai' do
53
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('openai')
54
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-test')
55
+ provider = described_class.new.build_provider
56
+ expect(provider).to be_a(Llm::Providers::OpenaiProvider)
57
+ end
58
+
59
+ it 'builds AnthropicProvider for anthropic' do
60
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('anthropic')
61
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-ant-test')
62
+ provider = described_class.new.build_provider
63
+ expect(provider).to be_a(Llm::Providers::AnthropicProvider)
64
+ end
65
+
66
+ it 'builds OllamaProvider for ollama' do
67
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('ollama')
68
+ provider = described_class.new.build_provider
69
+ expect(provider).to be_a(Llm::Providers::OllamaProvider)
70
+ end
71
+ end
72
+
73
+ describe 'config file fallback' do
74
+ it 'reads from config/config.yml when env vars are not set' do
75
+ allow(File).to receive(:exist?).with('config/config.yml').and_return(true)
76
+ allow(YAML).to receive(:load_file).with('config/config.yml').and_return(
77
+ 'llm_provider' => 'ollama',
78
+ 'llm_model' => 'codellama'
79
+ )
80
+ config = described_class.new
81
+ expect(config.provider_name).to eq('ollama')
82
+ expect(config.model).to eq('codellama')
83
+ end
84
+
85
+ it 'env vars take precedence over config file' do
86
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('anthropic')
87
+ allow(File).to receive(:exist?).with('config/config.yml').and_return(true)
88
+ allow(YAML).to receive(:load_file).with('config/config.yml').and_return('llm_provider' => 'ollama')
89
+ expect(described_class.new.provider_name).to eq('anthropic')
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/prompts'
4
+
5
+ RSpec.describe Llm::Prompts do
6
+ describe '.analyze_page' do
7
+ it 'includes URL and HTML in prompt' do
8
+ prompt = described_class.analyze_page('<html>test</html>', 'https://example.com')
9
+ expect(prompt).to include('https://example.com')
10
+ expect(prompt).to include('<html>test</html>')
11
+ end
12
+
13
+ it 'truncates HTML at 8000 characters' do
14
+ long_html = 'x' * 10_000
15
+ prompt = described_class.analyze_page(long_html, 'https://example.com')
16
+ expect(prompt.length).to be < 10_000
17
+ end
18
+
19
+ it 'requests JSON output' do
20
+ prompt = described_class.analyze_page('<html></html>', 'https://example.com')
21
+ expect(prompt).to include('JSON')
22
+ expect(prompt).to include('elements')
23
+ end
24
+ end
25
+
26
+ describe '.generate_test_scenarios' do
27
+ let(:methods) { [{ name: 'login', params: %w[user pass] }, { name: 'logout', params: [] }] }
28
+
29
+ it 'includes class and method info' do
30
+ prompt = described_class.generate_test_scenarios('LoginPage', methods, 'selenium', 'rspec')
31
+ expect(prompt).to include('LoginPage')
32
+ expect(prompt).to include('login(user, pass)')
33
+ expect(prompt).to include('logout')
34
+ end
35
+
36
+ it 'includes automation and framework context' do
37
+ prompt = described_class.generate_test_scenarios('LoginPage', methods, 'selenium', 'rspec')
38
+ expect(prompt).to include('selenium')
39
+ expect(prompt).to include('rspec')
40
+ end
41
+ end
42
+
43
+ describe '.system_prompt' do
44
+ it 'returns a non-empty string' do
45
+ expect(described_class.system_prompt).to be_a(String)
46
+ expect(described_class.system_prompt).not_to be_empty
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/response_parser'
4
+
5
+ RSpec.describe Llm::ResponseParser do
6
+ describe '.parse_json' do
7
+ it 'parses raw JSON' do
8
+ result = described_class.parse_json('{"key": "value"}')
9
+ expect(result).to eq(key: 'value')
10
+ end
11
+
12
+ it 'parses JSON from markdown code block' do
13
+ response = "```json\n{\"key\": \"value\"}\n```"
14
+ result = described_class.parse_json(response)
15
+ expect(result).to eq(key: 'value')
16
+ end
17
+
18
+ it 'extracts JSON from surrounding text' do
19
+ response = "Here is the result:\n{\"key\": \"value\"}\nDone."
20
+ result = described_class.parse_json(response)
21
+ expect(result).to eq(key: 'value')
22
+ end
23
+
24
+ it 'returns nil for nil input' do
25
+ expect(described_class.parse_json(nil)).to be_nil
26
+ end
27
+
28
+ it 'returns nil for empty input' do
29
+ expect(described_class.parse_json('')).to be_nil
30
+ end
31
+
32
+ it 'returns nil for invalid JSON' do
33
+ expect(described_class.parse_json('not json at all')).to be_nil
34
+ end
35
+
36
+ it 'returns nil for JSON arrays (not hashes)' do
37
+ expect(described_class.parse_json('[1, 2, 3]')).to be_nil
38
+ end
39
+ end
40
+
41
+ describe '.extract_elements' do
42
+ let(:valid_response) do
43
+ {
44
+ elements: [
45
+ { name: 'email_field', type: 'input', locator: { type: 'id', value: 'email' }, purpose: 'Email input' },
46
+ { name: 'submit_btn', type: 'button', locator: { type: 'id', value: 'submit' }, text: 'Submit' }
47
+ ]
48
+ }.to_json
49
+ end
50
+
51
+ it 'extracts and normalizes elements' do
52
+ elements = described_class.extract_elements(valid_response)
53
+ expect(elements.length).to eq(2)
54
+ expect(elements.first[:name]).to eq('email_field')
55
+ expect(elements.first[:locator][:type]).to eq(:id)
56
+ end
57
+
58
+ it 'skips elements without required fields' do
59
+ response = { elements: [{ name: 'incomplete' }] }.to_json
60
+ elements = described_class.extract_elements(response)
61
+ expect(elements).to be_empty
62
+ end
63
+
64
+ it 'returns nil for non-element response' do
65
+ expect(described_class.extract_elements('{"other": "data"}')).to be_nil
66
+ end
67
+
68
+ it 'returns nil for nil response' do
69
+ expect(described_class.extract_elements(nil)).to be_nil
70
+ end
71
+ end
72
+
73
+ describe '.extract_scenarios' do
74
+ let(:valid_response) do
75
+ {
76
+ scenarios: [
77
+ { method: 'login', description: 'logs in with valid credentials', assertion_hint: 'expect redirect' }
78
+ ]
79
+ }.to_json
80
+ end
81
+
82
+ it 'extracts scenarios' do
83
+ scenarios = described_class.extract_scenarios(valid_response)
84
+ expect(scenarios.length).to eq(1)
85
+ expect(scenarios.first[:method]).to eq('login')
86
+ end
87
+
88
+ it 'returns nil for non-scenario response' do
89
+ expect(described_class.extract_scenarios('{"other": "data"}')).to be_nil
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'rspec'
5
+ require_relative '../../lib/adopter/adopt_menu'
6
+
7
+ RSpec.describe Adopter::AdoptMenu do # rubocop:disable RSpec/FilePath
8
+ let(:prompt) { instance_double(TTY::Prompt) }
9
+ let(:source_dir) { 'tmp_adopt_interactive_source' }
10
+ let(:output_dir) { 'tmp_adopt_interactive_output' }
11
+
12
+ before do
13
+ allow(TTY::Prompt).to receive(:new).and_return(prompt)
14
+ allow(prompt).to receive(:say)
15
+ allow(prompt).to receive(:warn)
16
+ allow(prompt).to receive(:error)
17
+
18
+ FileUtils.mkdir_p(source_dir)
19
+ File.write("#{source_dir}/Gemfile", "gem 'rspec'\ngem 'selenium-webdriver'\n")
20
+ FileUtils.mkdir_p("#{source_dir}/spec")
21
+ File.write("#{source_dir}/spec/login_spec.rb", "describe 'Login' do\n it('works') { expect(true).to be true }\nend\n")
22
+ end
23
+
24
+ after do
25
+ FileUtils.rm_rf(source_dir)
26
+ FileUtils.rm_rf(output_dir)
27
+ end
28
+
29
+ describe '#run' do
30
+ before do
31
+ allow(prompt).to receive(:ask).and_return(source_dir, output_dir)
32
+ allow(prompt).to receive(:select)
33
+ .with('Select target automation framework:', anything, anything).and_return('Selenium')
34
+ allow(prompt).to receive(:select)
35
+ .with('Select target test framework:', anything, anything).and_return('Rspec')
36
+ allow(prompt).to receive(:select)
37
+ .with('Configure CI/CD?', anything, anything).and_return(nil)
38
+ end
39
+
40
+ it 'shows detection preview' do
41
+ described_class.new.run
42
+
43
+ expect(prompt).to have_received(:say).with(match(/Detected project settings/))
44
+ expect(prompt).to have_received(:say).with(match(/Automation: selenium/))
45
+ expect(prompt).to have_received(:say).with(match(/Framework:.*rspec/))
46
+ end
47
+
48
+ it 'executes adoption and shows results' do
49
+ described_class.new.run
50
+
51
+ expect(prompt).to have_received(:say).with(match(/Adoption complete/))
52
+ expect(prompt).to have_received(:say).with(match(/Pages converted/))
53
+ expect(prompt).to have_received(:say).with(match(/Tests converted/))
54
+ end
55
+
56
+ it 'generates the output project' do
57
+ described_class.new.run
58
+
59
+ expect(File).to exist("#{output_dir}/Gemfile")
60
+ end
61
+
62
+ it 'shows output path and bundle install instruction' do
63
+ described_class.new.run
64
+
65
+ expect(prompt).to have_received(:say).with(match(/Output: #{output_dir}/))
66
+ expect(prompt).to have_received(:say).with(match(/bundle install/))
67
+ end
68
+ end
69
+
70
+ describe '#run with mobile source project' do
71
+ before do
72
+ File.write("#{source_dir}/Gemfile", "gem 'rspec'\ngem 'appium_lib'\n")
73
+ allow(prompt).to receive(:ask).and_return(source_dir, output_dir)
74
+ allow(prompt).to receive(:select).and_return('Selenium', 'Rspec', nil)
75
+ end
76
+
77
+ it 'shows MobileProjectError message' do
78
+ described_class.new.run
79
+
80
+ expect(prompt).to have_received(:error).with(match(/Mobile.*Appium.*cannot be adopted/))
81
+ end
82
+ end
83
+
84
+ describe 'validation constants' do
85
+ it 'defines web automations' do
86
+ expect(described_class::WEB_AUTOMATIONS).to contain_exactly('selenium', 'capybara', 'watir')
87
+ end
88
+
89
+ it 'defines test frameworks' do
90
+ expect(described_class::TEST_FRAMEWORKS).to contain_exactly('rspec', 'cucumber', 'minitest')
91
+ end
92
+
93
+ it 'defines CI platforms' do
94
+ expect(described_class::CI_PLATFORMS).to contain_exactly('github', 'gitlab')
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/generators/menu_generator'
5
+
6
+ RSpec.describe MenuGenerator do
7
+ let(:prompt) { instance_double(TTY::Prompt) }
8
+ let(:menu_generator) { described_class.new('test_project') }
9
+
10
+ before do
11
+ allow(TTY::Prompt).to receive(:new).and_return(prompt)
12
+ end
13
+
14
+ describe '#generate_choice_menu' do
15
+ it 'presents automation framework selection' do
16
+ menu = double('menu') # rubocop:disable RSpec/VerifiedDoubles
17
+ allow(menu).to receive(:choice)
18
+ allow(prompt).to receive(:select).with('Please select your automation framework').and_yield(menu)
19
+
20
+ menu_generator.generate_choice_menu
21
+
22
+ expect(menu).to have_received(:choice).with(:Selenium, anything)
23
+ expect(menu).to have_received(:choice).with(:Capybara, anything)
24
+ expect(menu).to have_received(:choice).with(:Appium, anything)
25
+ expect(menu).to have_received(:choice).with(:Watir, anything)
26
+ expect(menu).to have_received(:choice).with(:Quit, anything)
27
+ end
28
+ end
29
+
30
+ describe '#choose_test_framework' do
31
+ context 'with a web automation' do
32
+ it 'presents test framework selection' do
33
+ menu = double('menu') # rubocop:disable RSpec/VerifiedDoubles
34
+ allow(menu).to receive(:choice)
35
+ allow(prompt).to receive(:select).with('Please select your test framework').and_yield(menu)
36
+
37
+ menu_generator.choose_test_framework('selenium')
38
+
39
+ expect(menu).to have_received(:choice).with(:Cucumber, anything)
40
+ expect(menu).to have_received(:choice).with(:Rspec, anything)
41
+ expect(menu).to have_received(:choice).with(:Minitest, anything)
42
+ expect(menu).to have_received(:choice).with(:Quit, anything)
43
+ end
44
+ end
45
+
46
+ context 'with appium' do
47
+ it 'presents mobile platform selection' do
48
+ menu = double('menu') # rubocop:disable RSpec/VerifiedDoubles
49
+ allow(menu).to receive(:choice)
50
+ allow(prompt).to receive(:select).with('Please select your mobile platform').and_yield(menu)
51
+
52
+ menu_generator.choose_test_framework('appium')
53
+
54
+ expect(menu).to have_received(:choice).with(:iOS, anything)
55
+ expect(menu).to have_received(:choice).with(:Android, anything)
56
+ expect(menu).to have_received(:choice).with(:Cross_Platform, anything)
57
+ expect(menu).to have_received(:choice).with(:Quit, anything)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe '#set_up_framework' do
63
+ let(:options) do
64
+ { automation: 'selenium', framework: 'rspec', ci_platform: 'github', accessibility: true }
65
+ end
66
+
67
+ before do
68
+ allow(prompt).to receive(:yes?).and_return(false)
69
+ end
70
+
71
+ it 'calls generate_framework with correct structure' do
72
+ allow(menu_generator).to receive(:generate_framework)
73
+ allow(menu_generator).to receive(:system)
74
+
75
+ menu_generator.set_up_framework(options)
76
+
77
+ expect(menu_generator).to have_received(:generate_framework).with(
78
+ hash_including(
79
+ automation: 'selenium',
80
+ framework: 'rspec',
81
+ ci_platform: 'github',
82
+ accessibility: true,
83
+ name: 'test_project'
84
+ )
85
+ )
86
+ end
87
+
88
+ it 'runs bundle install in the generated project' do
89
+ allow(menu_generator).to receive(:generate_framework)
90
+ allow(menu_generator).to receive(:system)
91
+
92
+ menu_generator.set_up_framework(options)
93
+
94
+ expect(menu_generator).to have_received(:system).with('cd test_project && gem install bundler && bundle install')
95
+ end
96
+ end
97
+
98
+ describe 'full flow: selenium + rspec + no CI' do
99
+ it 'calls set_up_framework with correct options' do
100
+ allow(menu_generator).to receive(:generate_framework)
101
+ allow(menu_generator).to receive(:system)
102
+ allow(prompt).to receive(:say)
103
+ allow(prompt).to receive(:yes?).and_return(false)
104
+
105
+ menu_generator.send(:create_framework, 'Rspec', 'selenium')
106
+
107
+ expect(menu_generator).to have_received(:generate_framework).with(
108
+ hash_including(automation: 'selenium', framework: 'rspec', name: 'test_project')
109
+ )
110
+ end
111
+ end
112
+
113
+ describe 'full flow: capybara + minitest + github' do
114
+ it 'calls set_up_framework with correct options' do
115
+ allow(menu_generator).to receive(:generate_framework)
116
+ allow(menu_generator).to receive(:system)
117
+ allow(prompt).to receive(:say)
118
+ allow(prompt).to receive(:yes?).and_return(false)
119
+
120
+ menu_generator.send(:create_framework, 'Minitest', 'capybara', ci_platform: 'github')
121
+
122
+ expect(menu_generator).to have_received(:generate_framework).with(
123
+ hash_including(
124
+ automation: 'capybara',
125
+ framework: 'minitest',
126
+ ci_platform: 'github',
127
+ accessibility: false,
128
+ name: 'test_project'
129
+ )
130
+ )
131
+ end
132
+ end
133
+
134
+ describe 'full flow: watir + cucumber + gitlab' do
135
+ it 'calls set_up_framework with correct options' do
136
+ allow(menu_generator).to receive(:generate_framework)
137
+ allow(menu_generator).to receive(:system)
138
+ allow(prompt).to receive(:say)
139
+ allow(prompt).to receive(:yes?).and_return(false)
140
+
141
+ menu_generator.send(:create_framework, 'Cucumber', 'watir', ci_platform: 'gitlab')
142
+
143
+ expect(menu_generator).to have_received(:generate_framework).with(
144
+ hash_including(
145
+ automation: 'watir',
146
+ framework: 'cucumber',
147
+ ci_platform: 'gitlab',
148
+ accessibility: false,
149
+ name: 'test_project'
150
+ )
151
+ )
152
+ end
153
+ end
154
+
155
+ describe 'full flow: selenium + rspec + accessibility' do
156
+ it 'passes accessibility flag through' do
157
+ allow(menu_generator).to receive(:generate_framework)
158
+ allow(menu_generator).to receive(:system)
159
+ allow(prompt).to receive(:say)
160
+ allow(prompt).to receive(:yes?).and_return(false)
161
+
162
+ menu_generator.send(:create_framework, 'Rspec', 'selenium', accessibility: true)
163
+
164
+ expect(menu_generator).to have_received(:generate_framework).with(
165
+ hash_including(automation: 'selenium', framework: 'rspec', accessibility: true)
166
+ )
167
+ end
168
+ end
169
+
170
+ describe 'full flow: selenium + rspec + visual' do
171
+ it 'passes visual flag through' do
172
+ allow(menu_generator).to receive(:generate_framework)
173
+ allow(menu_generator).to receive(:system)
174
+ allow(prompt).to receive(:say)
175
+ allow(prompt).to receive(:yes?).and_return(false)
176
+
177
+ menu_generator.send(:create_framework, 'Rspec', 'selenium', visual: true)
178
+
179
+ expect(menu_generator).to have_received(:generate_framework).with(
180
+ hash_including(automation: 'selenium', framework: 'rspec', visual: true)
181
+ )
182
+ end
183
+ end
184
+
185
+ describe 'full flow: selenium + rspec + performance' do
186
+ it 'passes performance flag through' do
187
+ allow(menu_generator).to receive(:generate_framework)
188
+ allow(menu_generator).to receive(:system)
189
+ allow(prompt).to receive(:say)
190
+ allow(prompt).to receive(:yes?).and_return(false)
191
+
192
+ menu_generator.send(:create_framework, 'Rspec', 'selenium', performance: true)
193
+
194
+ expect(menu_generator).to have_received(:generate_framework).with(
195
+ hash_including(automation: 'selenium', framework: 'rspec', performance: true)
196
+ )
197
+ end
198
+ end
199
+
200
+ describe 'ruby version selection menu' do
201
+ it 'presents ruby version choices' do # rubocop:disable RSpec/MultipleExpectations
202
+ menu = double('menu') # rubocop:disable RSpec/VerifiedDoubles
203
+ allow(menu).to receive(:choice)
204
+ allow(prompt).to receive(:select).with('Select Ruby version for your project').and_yield(menu)
205
+
206
+ menu_generator.send(:select_ruby_version, 'Rspec', 'selenium')
207
+
208
+ expect(menu).to have_received(:choice).with(:'3.4 (latest)', anything)
209
+ expect(menu).to have_received(:choice).with(:'3.3', anything)
210
+ expect(menu).to have_received(:choice).with(:'3.2', anything)
211
+ expect(menu).to have_received(:choice).with(:'3.1', anything)
212
+ expect(menu).to have_received(:choice).with(:Quit, anything)
213
+ end
214
+ end
215
+
216
+ describe 'ruby_version propagation' do
217
+ it 'passes ruby_version through to generate_framework' do
218
+ allow(menu_generator).to receive(:generate_framework)
219
+ allow(menu_generator).to receive(:system)
220
+ allow(prompt).to receive(:say)
221
+ allow(prompt).to receive(:yes?).and_return(false)
222
+
223
+ menu_generator.send(:create_framework, 'Rspec', 'selenium', ruby_version: '3.3')
224
+
225
+ expect(menu_generator).to have_received(:generate_framework).with(
226
+ hash_including(ruby_version: '3.3')
227
+ )
228
+ end
229
+ end
230
+
231
+ describe 'reporter selection menu' do
232
+ it 'includes JSON and All reporter choices' do # rubocop:disable RSpec/MultipleExpectations
233
+ menu = double('menu') # rubocop:disable RSpec/VerifiedDoubles
234
+ allow(menu).to receive(:choice)
235
+ allow(prompt).to receive(:select).with('Select your test reporter').and_yield(menu)
236
+
237
+ menu_generator.send(:select_reporter, 'Rspec', 'selenium')
238
+
239
+ expect(menu).to have_received(:choice).with(:JSON, anything)
240
+ expect(menu).to have_received(:choice).with(:All, anything)
241
+ expect(menu).to have_received(:choice).with(:Allure, anything)
242
+ expect(menu).to have_received(:choice).with(:JUnit, anything)
243
+ expect(menu).to have_received(:choice).with(:Both, anything)
244
+ expect(menu).to have_received(:choice).with(:None, anything)
245
+ end
246
+ end
247
+
248
+ describe 'project name propagation' do
249
+ it 'uses the project name from initialization' do
250
+ generator = described_class.new('my_custom_project')
251
+ allow(generator).to receive(:generate_framework)
252
+ allow(generator).to receive(:system)
253
+ allow(prompt).to receive(:say)
254
+ allow(prompt).to receive(:yes?).and_return(false)
255
+
256
+ generator.send(:create_framework, 'Rspec', 'selenium')
257
+
258
+ expect(generator).to have_received(:generate_framework).with(
259
+ hash_including(name: 'my_custom_project')
260
+ )
261
+ end
262
+ end
263
+ end