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,775 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+ require_relative '../../lib/commands/scaffolding_commands'
6
+ require_relative '../../lib/scaffolding/scaffolding'
7
+
8
+ # Custom matchers (defined inline to avoid pulling in integration spec_helper)
9
+ RSpec::Matchers.define :have_frozen_string_literal do
10
+ match { |content| content.include?('# frozen_string_literal: true') }
11
+ failure_message { 'expected file to contain frozen_string_literal magic comment' }
12
+ end
13
+
14
+ RSpec::Matchers.define :have_valid_ruby_syntax do
15
+ match do |content|
16
+ RubyVM::InstructionSequence.compile(content)
17
+ true
18
+ rescue SyntaxError
19
+ false
20
+ end
21
+ failure_message { |_content| "expected valid Ruby syntax but got SyntaxError: #{$ERROR_INFO&.message}" }
22
+ end
23
+
24
+ # End-to-end scaffolding tests that verify the scaffold command creates the right
25
+ # files with the right content across all automation × framework combinations.
26
+ describe 'Scaffolding E2E' do # rubocop:disable RSpec/DescribeClass
27
+
28
+ WEB_AUTOMATIONS = %w[selenium capybara watir].freeze
29
+ TEST_FRAMEWORKS = %w[rspec cucumber minitest].freeze
30
+
31
+ # The scaffolding system's generate_default_scaffold checks for spec/ or features/.
32
+ # Minitest projects have test/ only — so the scaffold falls through to feature+steps.
33
+ def self.scaffold_style(framework)
34
+ case framework
35
+ when 'rspec' then :spec
36
+ when 'cucumber' then :feature
37
+ when 'minitest' then :feature # no spec/ dir → falls to else clause
38
+ end
39
+ end
40
+
41
+ # Build a minimal mock project that the scaffolding system can detect.
42
+ def self.build_mock_project(automation, framework) # rubocop:disable Metrics/MethodLength
43
+ dir = File.expand_path("tmp_scaffold_e2e_#{framework}_#{automation}")
44
+ FileUtils.rm_rf(dir)
45
+ FileUtils.mkdir_p("#{dir}/page_objects/pages")
46
+ FileUtils.mkdir_p("#{dir}/page_objects/abstract")
47
+ FileUtils.mkdir_p("#{dir}/page_objects/components")
48
+ FileUtils.mkdir_p("#{dir}/helpers")
49
+ FileUtils.mkdir_p("#{dir}/config")
50
+ FileUtils.mkdir_p("#{dir}/models/data")
51
+
52
+ case framework
53
+ when 'rspec' then FileUtils.mkdir_p("#{dir}/spec")
54
+ when 'cucumber' then FileUtils.mkdir_p("#{dir}/features/step_definitions")
55
+ FileUtils.mkdir_p("#{dir}/features/support")
56
+ when 'minitest' then FileUtils.mkdir_p("#{dir}/test")
57
+ end
58
+
59
+ gems = []
60
+ case automation
61
+ when 'selenium' then gems << "gem 'selenium-webdriver'"
62
+ when 'capybara' then gems << "gem 'capybara'"
63
+ when 'watir' then gems << "gem 'watir'"
64
+ end
65
+ case framework
66
+ when 'rspec' then gems << "gem 'rspec'"
67
+ when 'cucumber' then gems << "gem 'cucumber'"
68
+ when 'minitest' then gems << "gem 'minitest'"
69
+ end
70
+ File.write("#{dir}/Gemfile", gems.join("\n") + "\n")
71
+ File.write("#{dir}/config/config.yml", "browser: chrome\nurl: http://localhost:3000\n")
72
+ File.write("#{dir}/page_objects/abstract/page.rb", "class Page; end\n")
73
+ File.write("#{dir}/page_objects/abstract/component.rb", "class Component; end\n")
74
+
75
+ dir
76
+ end
77
+
78
+ # ──────────────────────────────────────────────
79
+ # Shared examples: Page object content
80
+ # ──────────────────────────────────────────────
81
+
82
+ shared_examples 'valid scaffolded page' do
83
+ it 'has frozen_string_literal' do
84
+ expect(page_content).to have_frozen_string_literal
85
+ end
86
+
87
+ it 'has valid Ruby syntax' do
88
+ expect(page_content).to have_valid_ruby_syntax
89
+ end
90
+
91
+ it 'inherits from Page' do
92
+ expect(page_content).to include('class CheckoutPage < Page')
93
+ end
94
+
95
+ it 'requires abstract page' do
96
+ expect(page_content).to include("require_relative '../abstract/page'")
97
+ end
98
+
99
+ it 'includes url method (config has url)' do
100
+ expect(page_content).to include("def url(_page)")
101
+ expect(page_content).to include("'checkout'")
102
+ end
103
+ end
104
+
105
+ shared_examples 'selenium scaffolded page' do
106
+ it 'has driver.find_element example' do
107
+ expect(page_content).to include('driver.find_element')
108
+ end
109
+
110
+ it 'has private section' do
111
+ expect(page_content).to include('private')
112
+ end
113
+
114
+ it 'does not have capybara fill_in' do
115
+ expect(page_content).not_to include('fill_in')
116
+ end
117
+
118
+ it 'does not have watir browser.text_field' do
119
+ expect(page_content).not_to include('browser.text_field')
120
+ end
121
+ end
122
+
123
+ shared_examples 'capybara scaffolded page' do
124
+ it 'has fill_in example' do
125
+ expect(page_content).to include('fill_in')
126
+ end
127
+
128
+ it 'has no private section' do
129
+ expect(page_content).not_to include('private')
130
+ end
131
+
132
+ it 'does not have selenium find_element' do
133
+ expect(page_content).not_to include('driver.find_element')
134
+ end
135
+
136
+ it 'does not have watir browser.text_field' do
137
+ expect(page_content).not_to include('browser.text_field')
138
+ end
139
+ end
140
+
141
+ shared_examples 'watir scaffolded page' do
142
+ it 'has browser.text_field example' do
143
+ expect(page_content).to include('browser.text_field')
144
+ end
145
+
146
+ it 'has private section' do
147
+ expect(page_content).to include('private')
148
+ end
149
+
150
+ it 'does not have selenium find_element' do
151
+ expect(page_content).not_to include('driver.find_element')
152
+ end
153
+
154
+ it 'does not have capybara fill_in' do
155
+ expect(page_content).not_to include('fill_in')
156
+ end
157
+ end
158
+
159
+ # ──────────────────────────────────────────────
160
+ # Shared examples: Spec content (RSpec)
161
+ # ──────────────────────────────────────────────
162
+
163
+ shared_examples 'valid scaffolded spec' do
164
+ it 'has frozen_string_literal' do
165
+ expect(spec_content).to have_frozen_string_literal
166
+ end
167
+
168
+ it 'has valid Ruby syntax' do
169
+ expect(spec_content).to have_valid_ruby_syntax
170
+ end
171
+
172
+ it 'describes the page class' do
173
+ expect(spec_content).to include("describe 'CheckoutPage'")
174
+ end
175
+
176
+ it 'requires the page object' do
177
+ expect(spec_content).to include("require_relative '../page_objects/pages/checkout'")
178
+ end
179
+
180
+ it 'has pending test' do
181
+ expect(spec_content).to include("pending 'implement test'")
182
+ end
183
+
184
+ it 'has before block with navigation' do
185
+ expect(spec_content).to include('before do')
186
+ end
187
+ end
188
+
189
+ shared_examples 'selenium scaffolded spec' do
190
+ it 'instantiates page with driver' do
191
+ expect(spec_content).to include('CheckoutPage.new(driver)')
192
+ end
193
+
194
+ it 'navigates with driver' do
195
+ expect(spec_content).to include('driver.navigate.to')
196
+ end
197
+
198
+ it 'does not use capybara visit' do
199
+ expect(spec_content).not_to match(/^\s+visit '/)
200
+ end
201
+ end
202
+
203
+ shared_examples 'capybara scaffolded spec' do
204
+ it 'instantiates page without arguments' do
205
+ expect(spec_content).to include('CheckoutPage.new')
206
+ expect(spec_content).not_to include('CheckoutPage.new(driver)')
207
+ expect(spec_content).not_to include('CheckoutPage.new(browser)')
208
+ end
209
+
210
+ it 'uses visit for navigation' do
211
+ expect(spec_content).to include("visit 'checkout'")
212
+ end
213
+ end
214
+
215
+ shared_examples 'watir scaffolded spec' do
216
+ it 'instantiates page with browser' do
217
+ expect(spec_content).to include('CheckoutPage.new(browser)')
218
+ end
219
+
220
+ it 'navigates with browser.goto' do
221
+ expect(spec_content).to include('browser.goto')
222
+ end
223
+ end
224
+
225
+ # ──────────────────────────────────────────────
226
+ # Shared examples: Feature content (Cucumber)
227
+ # ──────────────────────────────────────────────
228
+
229
+ shared_examples 'valid scaffolded feature' do
230
+ it 'has Feature keyword' do
231
+ expect(feature_content).to include('Feature: Checkout')
232
+ end
233
+
234
+ it 'has Scenario keyword' do
235
+ expect(feature_content).to include('Scenario: Scenario name')
236
+ end
237
+
238
+ it 'has Given/When/Then steps' do
239
+ expect(feature_content).to include('Given I am on the checkout page')
240
+ expect(feature_content).to include('When I perform an action')
241
+ expect(feature_content).to include('Then I see the expected result')
242
+ end
243
+ end
244
+
245
+ # ──────────────────────────────────────────────
246
+ # Shared examples: Steps content (Cucumber)
247
+ # ──────────────────────────────────────────────
248
+
249
+ shared_examples 'valid scaffolded steps' do
250
+ it 'has frozen_string_literal' do
251
+ expect(steps_content).to have_frozen_string_literal
252
+ end
253
+
254
+ it 'has valid Ruby syntax' do
255
+ expect(steps_content).to have_valid_ruby_syntax
256
+ end
257
+
258
+ it 'requires the page object' do
259
+ expect(steps_content).to include("require_relative '../../page_objects/pages/checkout'")
260
+ end
261
+
262
+ it 'has Given/When/Then blocks' do
263
+ expect(steps_content).to include("Given('I am on the checkout page')")
264
+ expect(steps_content).to include("When('I perform an action')")
265
+ expect(steps_content).to include("Then('I see the expected result')")
266
+ end
267
+
268
+ it 'has pending steps' do
269
+ expect(steps_content).to include("pending 'implement step'")
270
+ end
271
+ end
272
+
273
+ shared_examples 'selenium scaffolded steps' do
274
+ it 'instantiates page with driver' do
275
+ expect(steps_content).to include('CheckoutPage.new(driver)')
276
+ end
277
+
278
+ it 'does not use capybara visit' do
279
+ expect(steps_content).not_to match(/^\s+visit '/)
280
+ end
281
+ end
282
+
283
+ shared_examples 'capybara scaffolded steps' do
284
+ it 'uses visit for Given step' do
285
+ expect(steps_content).to include("visit 'checkout'")
286
+ end
287
+
288
+ it 'does not instantiate with driver' do
289
+ expect(steps_content).not_to include('.new(driver)')
290
+ end
291
+ end
292
+
293
+ shared_examples 'watir scaffolded steps' do
294
+ it 'instantiates page with browser' do
295
+ expect(steps_content).to include('CheckoutPage.new(browser)')
296
+ end
297
+ end
298
+
299
+ # ──────────────────────────────────────────────
300
+ # Shared examples: Helper content
301
+ # ──────────────────────────────────────────────
302
+
303
+ shared_examples 'valid scaffolded helper' do
304
+ it 'has frozen_string_literal' do
305
+ expect(helper_content).to have_frozen_string_literal
306
+ end
307
+
308
+ it 'has valid Ruby syntax' do
309
+ expect(helper_content).to have_valid_ruby_syntax
310
+ end
311
+
312
+ it 'defines helper module' do
313
+ expect(helper_content).to include('module CheckoutHelper')
314
+ end
315
+
316
+ it 'has helper placeholder comment' do
317
+ expect(helper_content).to include('# Add your helper code here')
318
+ end
319
+ end
320
+
321
+ # ──────────────────────────────────────────────
322
+ # Shared examples: Component content
323
+ # ──────────────────────────────────────────────
324
+
325
+ shared_examples 'valid scaffolded component' do
326
+ it 'has frozen_string_literal' do
327
+ expect(component_content).to have_frozen_string_literal
328
+ end
329
+
330
+ it 'has valid Ruby syntax' do
331
+ expect(component_content).to have_valid_ruby_syntax
332
+ end
333
+
334
+ it 'inherits from Component' do
335
+ expect(component_content).to include('class Sidebar < Component')
336
+ end
337
+
338
+ it 'requires abstract component' do
339
+ expect(component_content).to include("require_relative '../abstract/component'")
340
+ end
341
+ end
342
+
343
+ shared_examples 'selenium scaffolded component' do
344
+ it 'has driver.find_element example' do
345
+ expect(component_content).to include('driver.find_element')
346
+ end
347
+
348
+ it 'has private section' do
349
+ expect(component_content).to include('private')
350
+ end
351
+
352
+ it 'has content method' do
353
+ expect(component_content).to include('def content')
354
+ end
355
+ end
356
+
357
+ shared_examples 'capybara scaffolded component' do
358
+ it 'has find(.selector) example' do
359
+ expect(component_content).to include("find('.selector')")
360
+ end
361
+
362
+ it 'has no private section' do
363
+ expect(component_content).not_to include('private')
364
+ end
365
+
366
+ it 'does not have driver.find_element' do
367
+ expect(component_content).not_to include('driver.find_element')
368
+ end
369
+ end
370
+
371
+ shared_examples 'watir scaffolded component' do
372
+ it 'has browser.element example' do
373
+ expect(component_content).to include('browser.element')
374
+ end
375
+
376
+ it 'has private section' do
377
+ expect(component_content).to include('private')
378
+ end
379
+
380
+ it 'has content method' do
381
+ expect(component_content).to include('def content')
382
+ end
383
+ end
384
+
385
+ # ══════════════════════════════════════════════
386
+ # Test matrix: automation × framework
387
+ # ══════════════════════════════════════════════
388
+
389
+ WEB_AUTOMATIONS.each do |automation|
390
+ TEST_FRAMEWORKS.each do |framework|
391
+ style = scaffold_style(framework)
392
+
393
+ context "with #{framework} and #{automation}" do # rubocop:disable RSpec/ContextWording
394
+ let(:scaffold) { ScaffoldingCommands }
395
+
396
+ # Use absolute path so Dir.chdir is safe regardless of current CWD
397
+ project_dir = build_mock_project(automation, framework)
398
+
399
+ before do
400
+ Dir.chdir(project_dir)
401
+ end
402
+
403
+ after do
404
+ Dir.chdir(File.dirname(project_dir))
405
+ end
406
+
407
+ after(:all) do # rubocop:disable RSpec/BeforeAfterAll
408
+ FileUtils.rm_rf(project_dir)
409
+ end
410
+
411
+ # ── Default scaffold ───────────────────
412
+
413
+ describe 'default scaffold' do
414
+ before do
415
+ scaffold.new.invoke(:scaffold, nil, %w[checkout])
416
+ end
417
+
418
+ after do
419
+ FileUtils.rm_f('page_objects/pages/checkout.rb')
420
+ FileUtils.rm_f('spec/checkout_page_spec.rb')
421
+ if framework == 'cucumber'
422
+ FileUtils.rm_f('features/checkout.feature')
423
+ FileUtils.rm_f('features/step_definitions/checkout_steps.rb')
424
+ else
425
+ FileUtils.rm_rf('features')
426
+ FileUtils.rm_rf('spec') unless framework == 'rspec'
427
+ end
428
+ end
429
+
430
+ it 'creates page object' do
431
+ expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
432
+ end
433
+
434
+ if style == :spec
435
+ it 'creates spec file' do
436
+ expect(Pathname.new('spec/checkout_page_spec.rb')).to be_file
437
+ end
438
+
439
+ it 'does not create feature file' do
440
+ expect(Pathname.new('features/checkout.feature')).not_to be_file
441
+ end
442
+ else # :feature
443
+ it 'creates feature file' do
444
+ expect(Pathname.new('features/checkout.feature')).to be_file
445
+ end
446
+
447
+ it 'creates steps file' do
448
+ expect(Pathname.new('features/step_definitions/checkout_steps.rb')).to be_file
449
+ end
450
+ end
451
+
452
+ # Page content validation
453
+ let(:page_content) { File.read('page_objects/pages/checkout.rb') }
454
+
455
+ include_examples 'valid scaffolded page'
456
+ include_examples "#{automation} scaffolded page"
457
+
458
+ # Test content validation (depends on scaffold style)
459
+ if style == :spec
460
+ let(:spec_content) { File.read('spec/checkout_page_spec.rb') }
461
+ include_examples 'valid scaffolded spec'
462
+ include_examples "#{automation} scaffolded spec"
463
+ else # :feature
464
+ let(:feature_content) { File.read('features/checkout.feature') }
465
+ include_examples 'valid scaffolded feature'
466
+
467
+ let(:steps_content) { File.read('features/step_definitions/checkout_steps.rb') }
468
+ include_examples 'valid scaffolded steps'
469
+ include_examples "#{automation} scaffolded steps"
470
+ end
471
+ end
472
+
473
+ # ── Component scaffold ─────────────────
474
+
475
+ describe 'component scaffold' do
476
+ before do
477
+ scaffold.new.invoke(:component, nil, %w[sidebar])
478
+ end
479
+
480
+ after do
481
+ FileUtils.rm_f('page_objects/components/sidebar.rb')
482
+ end
483
+
484
+ let(:component_content) { File.read('page_objects/components/sidebar.rb') }
485
+
486
+ include_examples 'valid scaffolded component'
487
+ include_examples "#{automation} scaffolded component"
488
+ end
489
+
490
+ # ── Helper scaffold ────────────────────
491
+
492
+ describe 'helper scaffold' do
493
+ before do
494
+ scaffold.new.invoke(:helper, nil, %w[checkout])
495
+ end
496
+
497
+ after do
498
+ FileUtils.rm_f('helpers/checkout_helper.rb')
499
+ end
500
+
501
+ let(:helper_content) { File.read('helpers/checkout_helper.rb') }
502
+
503
+ include_examples 'valid scaffolded helper'
504
+ end
505
+
506
+ # ── CRUD scaffold ──────────────────────
507
+ # CrudGenerator checks Dir.exist?('features'), Dir.exist?('test') separately.
508
+
509
+ describe 'CRUD scaffold' do
510
+ before do
511
+ scaffold.new.invoke(:scaffold, nil, %w[product --crud])
512
+ end
513
+
514
+ after do
515
+ %w[product_list product_create product_detail product_edit].each do |page|
516
+ FileUtils.rm_f("page_objects/pages/#{page}.rb")
517
+ FileUtils.rm_f("spec/#{page}_page_spec.rb")
518
+ FileUtils.rm_f("features/#{page}.feature")
519
+ FileUtils.rm_f("features/step_definitions/#{page}_steps.rb")
520
+ end
521
+ FileUtils.rm_rf('features') unless framework == 'cucumber'
522
+ FileUtils.rm_rf('spec') unless framework == 'rspec'
523
+ FileUtils.rm_f('models/data/product.yml')
524
+ end
525
+
526
+ it 'creates pages for all CRUD actions' do
527
+ %w[product_list product_create product_detail product_edit].each do |page|
528
+ expect(Pathname.new("page_objects/pages/#{page}.rb")).to be_file
529
+ end
530
+ end
531
+
532
+ it 'creates model data file' do
533
+ expect(Pathname.new('models/data/product.yml')).to be_file
534
+ end
535
+
536
+ it 'model data has expected sections' do
537
+ content = File.read('models/data/product.yml')
538
+ expect(content).to include('default:')
539
+ expect(content).to include('valid:')
540
+ expect(content).to include('invalid:')
541
+ end
542
+
543
+ if framework == 'cucumber'
544
+ it 'creates feature files for all CRUD actions' do
545
+ %w[product_list product_create product_detail product_edit].each do |page|
546
+ expect(Pathname.new("features/#{page}.feature")).to be_file
547
+ end
548
+ end
549
+
550
+ it 'creates step files for all CRUD actions' do
551
+ %w[product_list product_create product_detail product_edit].each do |page|
552
+ expect(Pathname.new("features/step_definitions/#{page}_steps.rb")).to be_file
553
+ end
554
+ end
555
+ elsif framework == 'rspec'
556
+ it 'creates spec files for all CRUD actions' do
557
+ %w[product_list product_create product_detail product_edit].each do |page|
558
+ expect(Pathname.new("spec/#{page}_page_spec.rb")).to be_file
559
+ end
560
+ end
561
+ else # minitest — CrudGenerator checks test/ and generates specs
562
+ it 'creates spec files for all CRUD actions' do
563
+ %w[product_list product_create product_detail product_edit].each do |page|
564
+ expect(Pathname.new("spec/#{page}_page_spec.rb")).to be_file
565
+ end
566
+ end
567
+ end
568
+
569
+ it 'all CRUD pages have valid Ruby syntax' do
570
+ %w[product_list product_create product_detail product_edit].each do |page|
571
+ content = File.read("page_objects/pages/#{page}.rb")
572
+ expect(content).to have_valid_ruby_syntax
573
+ end
574
+ end
575
+
576
+ it 'all CRUD pages have frozen_string_literal' do
577
+ %w[product_list product_create product_detail product_edit].each do |page|
578
+ content = File.read("page_objects/pages/#{page}.rb")
579
+ expect(content).to have_frozen_string_literal
580
+ end
581
+ end
582
+ end
583
+
584
+ # ── Selective --with ───────────────────
585
+
586
+ describe 'selective scaffold' do
587
+ after do
588
+ %w[settings payment order].each do |name|
589
+ FileUtils.rm_f("page_objects/pages/#{name}.rb")
590
+ FileUtils.rm_f("spec/#{name}_page_spec.rb")
591
+ FileUtils.rm_f("helpers/#{name}_helper.rb")
592
+ FileUtils.rm_f("models/data/#{name}.yml")
593
+ end
594
+ FileUtils.rm_rf('features') unless framework == 'cucumber'
595
+ FileUtils.rm_rf('spec') unless framework == 'rspec'
596
+ end
597
+
598
+ it 'generates only page when --with page' do
599
+ scaffold.new.invoke(:scaffold, nil, %w[settings --with page])
600
+ expect(Pathname.new('page_objects/pages/settings.rb')).to be_file
601
+ expect(Pathname.new('spec/settings_page_spec.rb')).not_to be_file
602
+ expect(Pathname.new('features/settings.feature')).not_to be_file
603
+ end
604
+
605
+ it 'generates page and helper with --with page helper' do
606
+ scaffold.new.invoke(:scaffold, nil, %w[payment --with page helper])
607
+ expect(Pathname.new('page_objects/pages/payment.rb')).to be_file
608
+ expect(Pathname.new('helpers/payment_helper.rb')).to be_file
609
+ end
610
+
611
+ it 'generates model data with --with model' do
612
+ scaffold.new.invoke(:scaffold, nil, %w[order --with model])
613
+ expect(Pathname.new('models/data/order.yml')).to be_file
614
+ end
615
+ end
616
+
617
+ # ── Destroy ────────────────────────────
618
+
619
+ describe 'destroy' do
620
+ it 'removes scaffolded files' do
621
+ scaffold.new.invoke(:scaffold, nil, %w[temp_item])
622
+ expect(Pathname.new('page_objects/pages/temp_item.rb')).to be_file
623
+
624
+ scaffold.new.invoke(:destroy, nil, %w[temp_item])
625
+ expect(Pathname.new('page_objects/pages/temp_item.rb')).not_to be_file
626
+
627
+ if style == :spec
628
+ expect(Pathname.new('spec/temp_item_page_spec.rb')).not_to be_file
629
+ else
630
+ expect(Pathname.new('features/temp_item.feature')).not_to be_file
631
+ expect(Pathname.new('features/step_definitions/temp_item_steps.rb')).not_to be_file
632
+ end
633
+ end
634
+ end
635
+
636
+ # ── Batch scaffold ─────────────────────
637
+
638
+ describe 'batch scaffold' do
639
+ after do
640
+ %w[search filter].each do |name|
641
+ FileUtils.rm_f("page_objects/pages/#{name}.rb")
642
+ FileUtils.rm_f("spec/#{name}_page_spec.rb")
643
+ FileUtils.rm_f("features/#{name}.feature")
644
+ FileUtils.rm_f("features/step_definitions/#{name}_steps.rb")
645
+ end
646
+ FileUtils.rm_rf('features') unless framework == 'cucumber'
647
+ FileUtils.rm_rf('spec') unless framework == 'rspec'
648
+ end
649
+
650
+ it 'generates multiple scaffolds at once' do
651
+ scaffold.new.invoke(:scaffold, nil, %w[search filter])
652
+ expect(Pathname.new('page_objects/pages/search.rb')).to be_file
653
+ expect(Pathname.new('page_objects/pages/filter.rb')).to be_file
654
+
655
+ if style == :spec
656
+ expect(Pathname.new('spec/search_page_spec.rb')).to be_file
657
+ expect(Pathname.new('spec/filter_page_spec.rb')).to be_file
658
+ else
659
+ expect(Pathname.new('features/search.feature')).to be_file
660
+ expect(Pathname.new('features/filter.feature')).to be_file
661
+ end
662
+ end
663
+ end
664
+
665
+ # ── Name normalization ─────────────────
666
+
667
+ describe 'name normalization' do
668
+ after do
669
+ %w[cart shopping_cart checkout].each do |name|
670
+ FileUtils.rm_f("page_objects/pages/#{name}.rb")
671
+ end
672
+ end
673
+
674
+ it 'strips _page suffix' do
675
+ scaffold.new.invoke(:page, nil, %w[cart_page])
676
+ expect(Pathname.new('page_objects/pages/cart.rb')).to be_file
677
+ end
678
+
679
+ it 'converts CamelCase to snake_case' do
680
+ scaffold.new.invoke(:page, nil, %w[ShoppingCart])
681
+ expect(Pathname.new('page_objects/pages/shopping_cart.rb')).to be_file
682
+ end
683
+
684
+ it 'handles CamelCase with Page suffix' do
685
+ scaffold.new.invoke(:page, nil, %w[CheckoutPage])
686
+ expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
687
+ end
688
+ end
689
+
690
+ # ── Relationships ──────────────────────
691
+
692
+ describe 'relationships' do
693
+ before do
694
+ File.write('page_objects/pages/login.rb', "class LoginPage < Page; end\n")
695
+ end
696
+
697
+ after do
698
+ FileUtils.rm_f('page_objects/pages/dashboard.rb')
699
+ FileUtils.rm_f('spec/dashboard_page_spec.rb')
700
+ FileUtils.rm_f('features/dashboard.feature')
701
+ FileUtils.rm_f('features/step_definitions/dashboard_steps.rb')
702
+ FileUtils.rm_f('page_objects/pages/login.rb')
703
+ FileUtils.rm_rf('features') unless framework == 'cucumber'
704
+ FileUtils.rm_rf('spec') unless framework == 'rspec'
705
+ end
706
+
707
+ it 'adds require_relative for dependent page' do
708
+ scaffold.new.invoke(:page, nil, %w[dashboard --uses login])
709
+ content = File.read('page_objects/pages/dashboard.rb')
710
+ expect(content).to include("require_relative 'login'")
711
+ end
712
+
713
+ if style == :spec
714
+ it 'adds dependency to spec let declarations' do
715
+ scaffold.new.invoke(:spec, nil, %w[dashboard --uses login])
716
+ content = File.read('spec/dashboard_page_spec.rb')
717
+ expect(content).to include('LoginPage')
718
+
719
+ case automation
720
+ when 'selenium' then expect(content).to include('LoginPage.new(driver)')
721
+ when 'watir' then expect(content).to include('LoginPage.new(browser)')
722
+ when 'capybara' then expect(content).to include('LoginPage.new')
723
+ end
724
+ end
725
+ end
726
+ end
727
+
728
+ # ── Template overrides ─────────────────
729
+
730
+ describe 'template overrides' do
731
+ after do
732
+ FileUtils.rm_rf('.ruby_raider')
733
+ FileUtils.rm_f('page_objects/pages/custom_override.rb')
734
+ FileUtils.rm_f('page_objects/pages/default_fallback.rb')
735
+ end
736
+
737
+ it 'uses override template when present' do
738
+ FileUtils.mkdir_p('.ruby_raider/templates')
739
+ File.write('.ruby_raider/templates/page_object.tt', <<~ERB)
740
+ # frozen_string_literal: true
741
+
742
+ class <%= page_class_name %> < Page
743
+ # E2E_CUSTOM_MARKER
744
+ end
745
+ ERB
746
+
747
+ scaffold.new.invoke(:page, nil, %w[custom_override])
748
+ content = File.read('page_objects/pages/custom_override.rb')
749
+ expect(content).to include('E2E_CUSTOM_MARKER')
750
+ expect(content).to include('class CustomOverridePage < Page')
751
+ end
752
+
753
+ it 'falls back to default template without override' do
754
+ scaffold.new.invoke(:page, nil, %w[default_fallback])
755
+ content = File.read('page_objects/pages/default_fallback.rb')
756
+ expect(content).not_to include('E2E_CUSTOM_MARKER')
757
+ expect(content).to include('class DefaultFallbackPage < Page')
758
+ end
759
+ end
760
+
761
+ # ── Dry run ────────────────────────────
762
+
763
+ describe 'dry run' do
764
+ it 'does not create files with --dry-run' do
765
+ expect do
766
+ scaffold.new.invoke(:page, nil, %w[phantom --dry-run])
767
+ end.to output(/phantom/).to_stdout
768
+
769
+ expect(Pathname.new('page_objects/pages/phantom.rb')).not_to be_file
770
+ end
771
+ end
772
+ end
773
+ end
774
+ end
775
+ end