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,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,104 @@
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 '.detect' do
91
+ before do
92
+ File.write('Gemfile', "gem 'selenium-webdriver'\ngem 'rspec'\n")
93
+ FileUtils.mkdir_p('spec')
94
+ end
95
+
96
+ it 'returns full detection hash' do
97
+ result = described_class.detect
98
+ expect(result[:automation]).to eq('selenium')
99
+ expect(result[:framework]).to eq('rspec')
100
+ expect(result[:has_spec]).to be true
101
+ expect(result[:has_features]).to be false
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,311 @@
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
+ RSpec.describe 'Scaffolding features' do
9
+ let(:scaffold) { ScaffoldingCommands }
10
+ let(:tmp_dir) { 'tmp_scaffold_features_test' }
11
+
12
+ before do
13
+ FileUtils.mkdir_p("#{tmp_dir}/page_objects/pages")
14
+ FileUtils.mkdir_p("#{tmp_dir}/page_objects/abstract")
15
+ FileUtils.mkdir_p("#{tmp_dir}/page_objects/components")
16
+ FileUtils.mkdir_p("#{tmp_dir}/spec")
17
+ FileUtils.mkdir_p("#{tmp_dir}/helpers")
18
+ FileUtils.mkdir_p("#{tmp_dir}/config")
19
+ FileUtils.mkdir_p("#{tmp_dir}/models/data")
20
+
21
+ File.write("#{tmp_dir}/Gemfile", "gem 'rspec'\ngem 'selenium-webdriver'\n")
22
+ File.write("#{tmp_dir}/config/config.yml", "browser: chrome\nurl: http://localhost:3000\n")
23
+ File.write("#{tmp_dir}/page_objects/abstract/page.rb", "class Page; end\n")
24
+ File.write("#{tmp_dir}/page_objects/abstract/component.rb", "class Component; end\n")
25
+
26
+ Dir.chdir(tmp_dir)
27
+ end
28
+
29
+ after do
30
+ Dir.chdir('..')
31
+ FileUtils.rm_rf(tmp_dir)
32
+ end
33
+
34
+ # --- Feature 7: Better name handling ---
35
+
36
+ describe 'name normalization' do
37
+ it 'strips _page suffix from page names' do
38
+ scaffold.new.invoke(:page, nil, %w[login_page])
39
+ expect(Pathname.new('page_objects/pages/login.rb')).to be_file
40
+ end
41
+
42
+ it 'handles CamelCase input' do
43
+ scaffold.new.invoke(:page, nil, %w[UserProfile])
44
+ expect(Pathname.new('page_objects/pages/user_profile.rb')).to be_file
45
+ end
46
+
47
+ it 'handles CamelCase with Page suffix' do
48
+ scaffold.new.invoke(:page, nil, %w[LoginPage])
49
+ expect(Pathname.new('page_objects/pages/login.rb')).to be_file
50
+ end
51
+ end
52
+
53
+ # --- Feature 6: Dry run ---
54
+
55
+ describe 'dry run' do
56
+ it 'does not create files with --dry-run' do
57
+ expect do
58
+ scaffold.new.invoke(:page, nil, %w[checkout --dry-run])
59
+ end.to output(/checkout/).to_stdout
60
+
61
+ expect(Pathname.new('page_objects/pages/checkout.rb')).not_to be_file
62
+ end
63
+
64
+ it 'shows planned file path' do
65
+ expect do
66
+ scaffold.new.invoke(:spec, nil, %w[checkout --dry-run])
67
+ end.to output(/checkout/).to_stdout
68
+ end
69
+ end
70
+
71
+ # --- Feature 8: Component scaffolding ---
72
+
73
+ describe 'component scaffolding' do
74
+ it 'creates a component file' do
75
+ scaffold.new.invoke(:component, nil, %w[sidebar])
76
+ expect(Pathname.new('page_objects/components/sidebar.rb')).to be_file
77
+ end
78
+
79
+ it 'component inherits from Component' do
80
+ scaffold.new.invoke(:component, nil, %w[sidebar])
81
+ content = File.read('page_objects/components/sidebar.rb')
82
+ expect(content).to include('class Sidebar < Component')
83
+ end
84
+
85
+ it 'component has frozen_string_literal' do
86
+ scaffold.new.invoke(:component, nil, %w[sidebar])
87
+ content = File.read('page_objects/components/sidebar.rb')
88
+ expect(content).to include('# frozen_string_literal: true')
89
+ end
90
+
91
+ it 'deletes a component' do
92
+ scaffold.new.invoke(:component, nil, %w[sidebar])
93
+ scaffold.new.invoke(:component, nil, %w[sidebar --delete])
94
+ expect(Pathname.new('page_objects/components/sidebar.rb')).not_to be_file
95
+ end
96
+ end
97
+
98
+ # --- Feature 10: Config-aware templates ---
99
+
100
+ describe 'config-aware templates' do
101
+ it 'page object includes url method when config has url' do
102
+ scaffold.new.invoke(:page, nil, %w[checkout])
103
+ content = File.read('page_objects/pages/checkout.rb')
104
+ expect(content).to include('def url(_page)')
105
+ end
106
+
107
+ it 'page object includes selenium-specific comments' do
108
+ scaffold.new.invoke(:page, nil, %w[checkout])
109
+ content = File.read('page_objects/pages/checkout.rb')
110
+ expect(content).to include('driver.find_element')
111
+ end
112
+
113
+ it 'spec includes let for page with driver' do
114
+ scaffold.new.invoke(:spec, nil, %w[checkout])
115
+ content = File.read('spec/checkout_page_spec.rb')
116
+ expect(content).to include('CheckoutPage.new(driver)')
117
+ end
118
+ end
119
+
120
+ # --- Feature 9: Relationships ---
121
+
122
+ describe 'relationships (--uses)' do
123
+ before do
124
+ File.write('page_objects/pages/login.rb', "class LoginPage < Page; end\n")
125
+ end
126
+
127
+ it 'adds require_relative for dependent pages' do
128
+ scaffold.new.invoke(:page, nil, %w[dashboard --uses login])
129
+ content = File.read('page_objects/pages/dashboard.rb')
130
+ expect(content).to include("require_relative 'login'")
131
+ end
132
+
133
+ it 'adds uses to spec as let declarations' do
134
+ scaffold.new.invoke(:spec, nil, %w[dashboard --uses login])
135
+ content = File.read('spec/dashboard_page_spec.rb')
136
+ expect(content).to include('LoginPage')
137
+ end
138
+ end
139
+
140
+ # --- Feature 1: Batch scaffold ---
141
+
142
+ describe 'batch scaffolding' do
143
+ it 'generates multiple scaffolds' do
144
+ scaffold.new.invoke(:scaffold, nil, %w[login dashboard])
145
+ expect(Pathname.new('page_objects/pages/login.rb')).to be_file
146
+ expect(Pathname.new('page_objects/pages/dashboard.rb')).to be_file
147
+ expect(Pathname.new('spec/login_page_spec.rb')).to be_file
148
+ expect(Pathname.new('spec/dashboard_page_spec.rb')).to be_file
149
+ end
150
+ end
151
+
152
+ # --- Feature 1: Selective --with ---
153
+
154
+ describe 'selective scaffolding (--with)' do
155
+ it 'generates only selected components' do
156
+ scaffold.new.invoke(:scaffold, nil, %w[checkout --with page])
157
+ expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
158
+ expect(Pathname.new('spec/checkout_page_spec.rb')).not_to be_file
159
+ end
160
+
161
+ it 'generates page and helper with --with' do
162
+ scaffold.new.invoke(:scaffold, nil, %w[checkout --with page helper])
163
+ expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
164
+ expect(Pathname.new('helpers/checkout_helper.rb')).to be_file
165
+ end
166
+
167
+ it 'generates model data with --with model' do
168
+ scaffold.new.invoke(:scaffold, nil, %w[user --with model])
169
+ expect(Pathname.new('models/data/user.yml')).to be_file
170
+ end
171
+ end
172
+
173
+ # --- Feature 5: CRUD ---
174
+
175
+ describe 'CRUD scaffolding' do
176
+ it 'generates CRUD pages' do
177
+ scaffold.new.invoke(:scaffold, nil, %w[user --crud])
178
+ %w[user_list user_create user_detail user_edit].each do |page|
179
+ expect(Pathname.new("page_objects/pages/#{page}.rb")).to be_file
180
+ end
181
+ end
182
+
183
+ it 'generates CRUD specs' do
184
+ scaffold.new.invoke(:scaffold, nil, %w[user --crud])
185
+ %w[user_list user_create user_detail user_edit].each do |page|
186
+ expect(Pathname.new("spec/#{page}_page_spec.rb")).to be_file
187
+ end
188
+ end
189
+
190
+ it 'generates model data file' do
191
+ scaffold.new.invoke(:scaffold, nil, %w[user --crud])
192
+ expect(Pathname.new('models/data/user.yml')).to be_file
193
+ end
194
+
195
+ it 'model data has expected structure' do
196
+ scaffold.new.invoke(:scaffold, nil, %w[user --crud])
197
+ content = File.read('models/data/user.yml')
198
+ expect(content).to include('default:')
199
+ expect(content).to include('valid:')
200
+ expect(content).to include('invalid:')
201
+ end
202
+ end
203
+
204
+ # --- Feature: Destroy command ---
205
+
206
+ describe 'destroy command' do
207
+ it 'removes page and spec files' do
208
+ scaffold.new.invoke(:scaffold, nil, %w[login])
209
+ expect(Pathname.new('page_objects/pages/login.rb')).to be_file
210
+ expect(Pathname.new('spec/login_page_spec.rb')).to be_file
211
+
212
+ scaffold.new.invoke(:destroy, nil, %w[login])
213
+ expect(Pathname.new('page_objects/pages/login.rb')).not_to be_file
214
+ expect(Pathname.new('spec/login_page_spec.rb')).not_to be_file
215
+ end
216
+
217
+ it 'removes only specified components with --with' do # rubocop:disable RSpec/MultipleExpectations
218
+ scaffold.new.invoke(:scaffold, nil, %w[checkout --with page spec helper])
219
+ expect(Pathname.new('page_objects/pages/checkout.rb')).to be_file
220
+ expect(Pathname.new('spec/checkout_page_spec.rb')).to be_file
221
+ expect(Pathname.new('helpers/checkout_helper.rb')).to be_file
222
+
223
+ scaffold.new.invoke(:destroy, nil, %w[checkout --with page helper])
224
+ expect(Pathname.new('page_objects/pages/checkout.rb')).not_to be_file
225
+ expect(Pathname.new('helpers/checkout_helper.rb')).not_to be_file
226
+ expect(Pathname.new('spec/checkout_page_spec.rb')).to be_file
227
+ end
228
+
229
+ it 'destroys multiple names at once' do
230
+ scaffold.new.invoke(:scaffold, nil, %w[login dashboard])
231
+ scaffold.new.invoke(:destroy, nil, %w[login dashboard])
232
+ expect(Pathname.new('page_objects/pages/login.rb')).not_to be_file
233
+ expect(Pathname.new('page_objects/pages/dashboard.rb')).not_to be_file
234
+ end
235
+ end
236
+
237
+ # --- Feature: Template overrides ---
238
+
239
+ describe 'template overrides' do
240
+ before do
241
+ FileUtils.mkdir_p('.ruby_raider/templates')
242
+ end
243
+
244
+ after do
245
+ FileUtils.rm_rf('.ruby_raider')
246
+ end
247
+
248
+ it 'uses override template when present' do
249
+ File.write('.ruby_raider/templates/page_object.tt', <<~ERB)
250
+ # Custom page for <%= class_name %>
251
+ class <%= page_class_name %> < Page
252
+ # CUSTOM OVERRIDE
253
+ end
254
+ ERB
255
+
256
+ Scaffolding.new(%w[widget]).generate_page
257
+ content = File.read('page_objects/pages/widget.rb')
258
+ expect(content).to include('CUSTOM OVERRIDE')
259
+ expect(content).to include('class WidgetPage < Page')
260
+ end
261
+
262
+ it 'falls back to default template when no override exists' do
263
+ Scaffolding.new(%w[gadget]).generate_page
264
+ content = File.read('page_objects/pages/gadget.rb')
265
+ expect(content).not_to include('CUSTOM OVERRIDE')
266
+ expect(content).to include('class GadgetPage < Page')
267
+ end
268
+ end
269
+
270
+ # --- Feature 3: Spec from page ---
271
+
272
+ describe 'spec from page (--from)' do
273
+ before do
274
+ File.write('page_objects/pages/login.rb', <<~RUBY)
275
+ class LoginPage < Page
276
+ def login(username, password)
277
+ username_field.send_keys username
278
+ end
279
+
280
+ def welcome_message
281
+ driver.find_element(css: '.welcome').text
282
+ end
283
+
284
+ private
285
+
286
+ def username_field
287
+ driver.find_element(id: 'username')
288
+ end
289
+ end
290
+ RUBY
291
+ end
292
+
293
+ it 'generates spec with method stubs' do
294
+ scaffold.new.invoke(:spec, nil, %w[login --from page_objects/pages/login.rb])
295
+ expect(Pathname.new('spec/login_spec.rb')).to be_file
296
+ end
297
+
298
+ it 'spec includes describe blocks for each public method' do
299
+ scaffold.new.invoke(:spec, nil, %w[login --from page_objects/pages/login.rb])
300
+ content = File.read('spec/login_spec.rb')
301
+ expect(content).to include("describe '#login'")
302
+ expect(content).to include("describe '#welcome_message'")
303
+ end
304
+
305
+ it 'spec does not include private methods' do
306
+ scaffold.new.invoke(:spec, nil, %w[login --from page_objects/pages/login.rb])
307
+ content = File.read('spec/login_spec.rb')
308
+ expect(content).not_to include('username_field')
309
+ end
310
+ end
311
+ end