ruby_raider 2.0.0 → 3.0.1

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 (228) 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 +246 -38
  20. data/lib/commands/utility_commands.rb +92 -3
  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 +13 -3
  71. data/lib/generators/templates/helpers/capybara_helper.tt +6 -2
  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 +9 -8
  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 +96 -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 +61 -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/commands/scaffolding_commands_spec.rb +22 -0
  178. data/spec/generators/generator_spec.rb +23 -0
  179. data/spec/integration/commands/scaffolding_commands_spec.rb +1 -1
  180. data/spec/integration/commands/utility_commands_spec.rb +29 -9
  181. data/spec/integration/content/ci_content_spec.rb +119 -0
  182. data/spec/integration/content/common_content_spec.rb +288 -0
  183. data/spec/integration/content/config_content_spec.rb +175 -0
  184. data/spec/integration/content/content_helper.rb +32 -0
  185. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  186. data/spec/integration/content/helper_content_spec.rb +485 -0
  187. data/spec/integration/content/page_content_spec.rb +259 -0
  188. data/spec/integration/content/reporter_content_spec.rb +236 -0
  189. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  190. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  191. data/spec/integration/content/test_content_spec.rb +266 -0
  192. data/spec/integration/end_to_end_features_spec.rb +690 -0
  193. data/spec/integration/end_to_end_spec.rb +52 -16
  194. data/spec/integration/generators/automation_generator_spec.rb +0 -12
  195. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  196. data/spec/integration/generators/common_generator_spec.rb +12 -13
  197. data/spec/integration/generators/config_features_spec.rb +155 -0
  198. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  199. data/spec/integration/generators/helpers_generator_spec.rb +0 -12
  200. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  201. data/spec/integration/generators/minitest_generator_spec.rb +0 -6
  202. data/spec/integration/generators/reporter_spec.rb +159 -0
  203. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  204. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  205. data/spec/integration/scaffolding_e2e_spec.rb +775 -0
  206. data/spec/integration/settings_helper.rb +0 -3
  207. data/spec/integration/spec_helper.rb +30 -13
  208. data/spec/llm/client_spec.rb +79 -0
  209. data/spec/llm/config_spec.rb +92 -0
  210. data/spec/llm/prompts_spec.rb +49 -0
  211. data/spec/llm/response_parser_spec.rb +92 -0
  212. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  213. data/spec/menus/menu_generator_spec.rb +263 -0
  214. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  215. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  216. data/spec/scaffolding/scaffold_project_detector_spec.rb +142 -0
  217. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  218. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  219. data/spec/system/adopt_matrix_spec.rb +537 -0
  220. data/spec/system/adopt_spec.rb +225 -0
  221. data/spec/system/support/system_test_helper.rb +0 -2
  222. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  223. data/spec/utilities/headless_config_spec.rb +89 -0
  224. data/spec/utilities/utilities_spec.rb +105 -0
  225. metadata +154 -5
  226. data/lib/generators/automation/templates/visual_options.tt +0 -16
  227. data/lib/generators/templates/helpers/partials/axe_driver.tt +0 -10
  228. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -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
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/scaffolding/name_normalizer'
4
+
5
+ RSpec.describe NameNormalizer do
6
+ describe '.normalize' do
7
+ it 'strips _page suffix' do
8
+ expect(described_class.normalize('login_page')).to eq('login')
9
+ end
10
+
11
+ it 'strips _spec suffix' do
12
+ expect(described_class.normalize('login_spec')).to eq('login')
13
+ end
14
+
15
+ it 'strips _steps suffix' do
16
+ expect(described_class.normalize('login_steps')).to eq('login')
17
+ end
18
+
19
+ it 'strips _helper suffix' do
20
+ expect(described_class.normalize('login_helper')).to eq('login')
21
+ end
22
+
23
+ it 'converts CamelCase to snake_case' do
24
+ expect(described_class.normalize('LoginPage')).to eq('login')
25
+ end
26
+
27
+ it 'handles CamelCase without suffix' do
28
+ expect(described_class.normalize('Dashboard')).to eq('dashboard')
29
+ end
30
+
31
+ it 'preserves nested paths' do
32
+ expect(described_class.normalize('admin/users')).to eq('admin/users')
33
+ end
34
+
35
+ it 'handles CamelCase nested paths' do
36
+ expect(described_class.normalize('Admin::UsersPage')).to eq('admin/users')
37
+ end
38
+
39
+ it 'leaves clean names unchanged' do
40
+ expect(described_class.normalize('login')).to eq('login')
41
+ end
42
+ end
43
+
44
+ describe '.to_class_name' do
45
+ it 'converts snake_case to CamelCase' do
46
+ expect(described_class.to_class_name('login')).to eq('Login')
47
+ end
48
+
49
+ it 'handles underscored names' do
50
+ expect(described_class.to_class_name('user_profile')).to eq('UserProfile')
51
+ end
52
+
53
+ it 'strips suffixes before converting' do
54
+ expect(described_class.to_class_name('login_page')).to eq('Login')
55
+ end
56
+
57
+ it 'handles nested names with modules' do
58
+ expect(described_class.to_class_name('admin/users')).to eq('Admin::Users')
59
+ end
60
+
61
+ it 'appends optional suffix' do
62
+ expect(described_class.to_class_name('login', 'Page')).to eq('LoginPage')
63
+ end
64
+ end
65
+
66
+ describe '.to_page_class' do
67
+ it 'converts to page class name' do
68
+ expect(described_class.to_page_class('login')).to eq('LoginPage')
69
+ end
70
+
71
+ it 'does not double the Page suffix' do
72
+ expect(described_class.to_page_class('login_page')).to eq('LoginPage')
73
+ end
74
+
75
+ it 'handles CamelCase input' do
76
+ expect(described_class.to_page_class('LoginPage')).to eq('LoginPage')
77
+ end
78
+ end
79
+
80
+ describe '.nested?' do
81
+ it 'returns true for nested paths' do
82
+ expect(described_class.nested?('admin/users')).to be true
83
+ end
84
+
85
+ it 'returns false for simple names' do
86
+ expect(described_class.nested?('login')).to be false
87
+ end
88
+ end
89
+
90
+ describe '.module_parts' do
91
+ it 'returns module path for nested names' do
92
+ expect(described_class.module_parts('admin/users')).to eq(['Admin'])
93
+ end
94
+
95
+ it 'returns empty for simple names' do
96
+ expect(described_class.module_parts('login')).to eq([])
97
+ end
98
+
99
+ it 'handles deep nesting' do
100
+ expect(described_class.module_parts('admin/settings/users')).to eq(%w[Admin Settings])
101
+ end
102
+ end
103
+
104
+ describe '.leaf_name' do
105
+ it 'returns last segment of nested path' do
106
+ expect(described_class.leaf_name('admin/users')).to eq('users')
107
+ end
108
+
109
+ it 'returns name itself for simple names' do
110
+ expect(described_class.leaf_name('login')).to eq('login')
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../../lib/scaffolding/page_introspector'
5
+
6
+ RSpec.describe PageIntrospector do
7
+ subject(:introspector) { described_class.new(page_file) }
8
+
9
+ let(:tmp_dir) { 'tmp_introspector_test' }
10
+ let(:page_file) { "#{tmp_dir}/login_page.rb" }
11
+
12
+ before do
13
+ FileUtils.mkdir_p(tmp_dir)
14
+ File.write(page_file, <<~RUBY)
15
+ class LoginPage < Page
16
+ def url(_page)
17
+ '/login'
18
+ end
19
+
20
+ def login(username, password)
21
+ username_field.send_keys username
22
+ password_field.send_keys password
23
+ login_button.click
24
+ end
25
+
26
+ def welcome_message
27
+ driver.find_element(css: '.welcome').text
28
+ end
29
+
30
+ private
31
+
32
+ def username_field
33
+ driver.find_element(id: 'username')
34
+ end
35
+
36
+ def password_field
37
+ driver.find_element(id: 'password')
38
+ end
39
+
40
+ def login_button
41
+ driver.find_element(id: 'login-btn')
42
+ end
43
+ end
44
+ RUBY
45
+ end
46
+
47
+ after do
48
+ FileUtils.rm_rf(tmp_dir)
49
+ end
50
+
51
+ it 'extracts class name' do
52
+ expect(introspector.class_name).to eq('LoginPage')
53
+ end
54
+
55
+ it 'extracts public methods' do
56
+ method_names = introspector.methods.map { |m| m[:name] }
57
+ expect(method_names).to include('login')
58
+ expect(method_names).to include('welcome_message')
59
+ end
60
+
61
+ it 'skips url method' do
62
+ method_names = introspector.methods.map { |m| m[:name] }
63
+ expect(method_names).not_to include('url')
64
+ end
65
+
66
+ it 'skips private methods' do
67
+ method_names = introspector.methods.map { |m| m[:name] }
68
+ expect(method_names).not_to include('username_field')
69
+ expect(method_names).not_to include('password_field')
70
+ expect(method_names).not_to include('login_button')
71
+ end
72
+
73
+ it 'extracts method parameters' do
74
+ login_method = introspector.methods.find { |m| m[:name] == 'login' }
75
+ expect(login_method[:params]).to eq(%w[username password])
76
+ end
77
+
78
+ it 'handles methods with no parameters' do
79
+ welcome = introspector.methods.find { |m| m[:name] == 'welcome_message' }
80
+ expect(welcome[:params]).to eq([])
81
+ end
82
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../../lib/scaffolding/project_detector'
5
+
6
+ RSpec.describe ScaffoldProjectDetector do
7
+ let(:tmp_dir) { 'tmp_project_detector_test' }
8
+
9
+ before do
10
+ FileUtils.mkdir_p(tmp_dir)
11
+ Dir.chdir(tmp_dir)
12
+ end
13
+
14
+ after do
15
+ Dir.chdir('..')
16
+ FileUtils.rm_rf(tmp_dir)
17
+ end
18
+
19
+ context 'with selenium project' do
20
+ before do
21
+ File.write('Gemfile', "gem 'rspec'\ngem 'selenium-webdriver'\n")
22
+ FileUtils.mkdir_p('spec')
23
+ end
24
+
25
+ it 'detects selenium automation' do
26
+ expect(described_class.detect_automation).to eq('selenium')
27
+ end
28
+
29
+ it 'detects rspec framework' do
30
+ expect(described_class.detect_framework).to eq('rspec')
31
+ end
32
+
33
+ it 'reports selenium?' do
34
+ expect(described_class.selenium?).to be true
35
+ end
36
+
37
+ it 'reports not capybara?' do
38
+ expect(described_class.capybara?).to be false
39
+ end
40
+ end
41
+
42
+ context 'with capybara project' do
43
+ before do
44
+ File.write('Gemfile', "gem 'capybara'\ngem 'cucumber'\n")
45
+ FileUtils.mkdir_p('features')
46
+ end
47
+
48
+ it 'detects capybara automation' do
49
+ expect(described_class.detect_automation).to eq('capybara')
50
+ end
51
+
52
+ it 'detects cucumber framework' do
53
+ expect(described_class.detect_framework).to eq('cucumber')
54
+ end
55
+
56
+ it 'reports capybara?' do
57
+ expect(described_class.capybara?).to be true
58
+ end
59
+ end
60
+
61
+ context 'with watir project' do
62
+ before do
63
+ File.write('Gemfile', "gem 'watir'\ngem 'minitest'\n")
64
+ FileUtils.mkdir_p('test')
65
+ end
66
+
67
+ it 'detects watir automation' do
68
+ expect(described_class.detect_automation).to eq('watir')
69
+ end
70
+
71
+ it 'detects minitest framework' do
72
+ expect(described_class.detect_framework).to eq('minitest')
73
+ end
74
+
75
+ it 'reports watir?' do
76
+ expect(described_class.watir?).to be true
77
+ end
78
+ end
79
+
80
+ context 'with no Gemfile' do
81
+ it 'defaults to selenium' do
82
+ expect(described_class.detect_automation).to eq('selenium')
83
+ end
84
+
85
+ it 'defaults to rspec' do
86
+ expect(described_class.detect_framework).to eq('rspec')
87
+ end
88
+ end
89
+
90
+ describe '.validate_project' do
91
+ it 'warns when Gemfile is missing' do
92
+ warnings = described_class.validate_project
93
+ expect(warnings).to include(match(/Gemfile not found/))
94
+ end
95
+
96
+ it 'warns when config/config.yml is missing' do
97
+ File.write('Gemfile', "gem 'rspec'\n")
98
+ warnings = described_class.validate_project
99
+ expect(warnings).to include(match(/config\.yml not found/))
100
+ end
101
+
102
+ it 'returns no warnings for valid project' do
103
+ File.write('Gemfile', "gem 'rspec'\n")
104
+ FileUtils.mkdir_p('config')
105
+ File.write('config/config.yml', "browser: chrome\n")
106
+ expect(described_class.validate_project).to be_empty
107
+ end
108
+ end
109
+
110
+ describe '.config' do
111
+ it 'returns empty hash when config missing' do
112
+ expect(described_class.config).to eq({})
113
+ end
114
+
115
+ it 'parses valid config' do
116
+ FileUtils.mkdir_p('config')
117
+ File.write('config/config.yml', "browser: chrome\nurl: http://localhost\n")
118
+ expect(described_class.config).to eq({ 'browser' => 'chrome', 'url' => 'http://localhost' })
119
+ end
120
+
121
+ it 'raises Error on malformed YAML' do
122
+ FileUtils.mkdir_p('config')
123
+ File.write('config/config.yml', "browser: chrome\n bad:\nindent")
124
+ expect { described_class.config }.to raise_error(ScaffoldProjectDetector::Error, /invalid YAML syntax/)
125
+ end
126
+ end
127
+
128
+ describe '.detect' do
129
+ before do
130
+ File.write('Gemfile', "gem 'selenium-webdriver'\ngem 'rspec'\n")
131
+ FileUtils.mkdir_p('spec')
132
+ end
133
+
134
+ it 'returns full detection hash' do
135
+ result = described_class.detect
136
+ expect(result[:automation]).to eq('selenium')
137
+ expect(result[:framework]).to eq('rspec')
138
+ expect(result[:has_spec]).to be true
139
+ expect(result[:has_features]).to be false
140
+ end
141
+ end
142
+ end