ruby_raider 1.1.4 → 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 (278) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +58 -0
  3. data/.github/workflows/integration.yml +4 -6
  4. data/.github/workflows/reek.yml +6 -5
  5. data/.github/workflows/release.yml +175 -0
  6. data/.github/workflows/rubocop.yml +7 -6
  7. data/.github/workflows/steep.yml +21 -0
  8. data/.github/workflows/system_tests.yml +83 -0
  9. data/.gitignore +1 -1
  10. data/.reek.yml +46 -4
  11. data/.rubocop.yml +24 -0
  12. data/.ruby-version +1 -1
  13. data/README.md +140 -77
  14. data/RELEASE.md +412 -0
  15. data/RELEASE_QUICK_GUIDE.md +77 -0
  16. data/Steepfile +22 -0
  17. data/assets/ruby_raider_logo.svg +51 -0
  18. data/bin/release +186 -0
  19. data/lib/adopter/adopt_menu.rb +146 -0
  20. data/lib/adopter/converters/base_converter.rb +84 -0
  21. data/lib/adopter/converters/identity_converter.rb +53 -0
  22. data/lib/adopter/migration_plan.rb +74 -0
  23. data/lib/adopter/migrator.rb +96 -0
  24. data/lib/adopter/plan_builder.rb +275 -0
  25. data/lib/adopter/project_analyzer.rb +252 -0
  26. data/lib/adopter/project_detector.rb +157 -0
  27. data/lib/commands/adopt_commands.rb +42 -0
  28. data/lib/commands/plugin_commands.rb +0 -2
  29. data/lib/commands/scaffolding_commands.rb +220 -37
  30. data/lib/commands/utility_commands.rb +82 -2
  31. data/lib/generators/automation/automation_generator.rb +0 -7
  32. data/lib/generators/automation/templates/account.tt +9 -5
  33. data/lib/generators/automation/templates/appium_caps.tt +60 -6
  34. data/lib/generators/automation/templates/home.tt +4 -4
  35. data/lib/generators/automation/templates/login.tt +61 -4
  36. data/lib/generators/automation/templates/page.tt +13 -7
  37. data/lib/generators/automation/templates/partials/element.tt +1 -1
  38. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -4
  39. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -8
  40. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -4
  41. data/lib/generators/automation/templates/partials/url_methods.tt +0 -1
  42. data/lib/generators/automation/templates/partials/visit_method.tt +11 -1
  43. data/lib/generators/automation/templates/pdp.tt +1 -1
  44. data/lib/generators/common_generator.rb +12 -0
  45. data/lib/generators/cucumber/cucumber_generator.rb +36 -0
  46. data/lib/generators/cucumber/templates/accessibility_feature.tt +5 -0
  47. data/lib/generators/cucumber/templates/accessibility_steps.tt +21 -0
  48. data/lib/generators/cucumber/templates/cucumber.tt +8 -1
  49. data/lib/generators/cucumber/templates/env.tt +6 -4
  50. data/lib/generators/cucumber/templates/feature.tt +0 -4
  51. data/lib/generators/cucumber/templates/partials/appium_env.tt +5 -0
  52. data/lib/generators/cucumber/templates/partials/capybara_env.tt +38 -0
  53. data/lib/generators/cucumber/templates/partials/capybara_world.tt +6 -0
  54. data/lib/generators/cucumber/templates/partials/driver_world.tt +1 -4
  55. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +2 -2
  56. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -35
  57. data/lib/generators/cucumber/templates/partials/watir_env.tt +20 -1
  58. data/lib/generators/cucumber/templates/partials/web_steps.tt +10 -15
  59. data/lib/generators/cucumber/templates/performance_feature.tt +5 -0
  60. data/lib/generators/cucumber/templates/performance_steps.tt +17 -0
  61. data/lib/generators/cucumber/templates/steps.tt +2 -2
  62. data/lib/generators/cucumber/templates/visual_feature.tt +5 -0
  63. data/lib/generators/cucumber/templates/visual_steps.tt +19 -0
  64. data/lib/generators/cucumber/templates/world.tt +5 -3
  65. data/lib/generators/generator.rb +50 -7
  66. data/lib/generators/helper_generator.rb +39 -9
  67. data/lib/generators/infrastructure/github_generator.rb +6 -0
  68. data/lib/generators/infrastructure/templates/github.tt +12 -8
  69. data/lib/generators/infrastructure/templates/github_appium.tt +108 -0
  70. data/lib/generators/infrastructure/templates/gitlab.tt +6 -3
  71. data/lib/generators/invoke_generators.rb +43 -9
  72. data/lib/generators/menu_generator.rb +122 -11
  73. data/lib/generators/minitest/minitest_generator.rb +35 -0
  74. data/lib/generators/minitest/templates/accessibility_test.tt +26 -0
  75. data/lib/generators/minitest/templates/performance_test.tt +18 -0
  76. data/lib/generators/minitest/templates/test.tt +64 -0
  77. data/lib/generators/minitest/templates/visual_test.tt +23 -0
  78. data/lib/generators/rspec/rspec_generator.rb +16 -4
  79. data/lib/generators/rspec/templates/accessibility_spec.tt +25 -0
  80. data/lib/generators/rspec/templates/performance_spec.tt +18 -0
  81. data/lib/generators/rspec/templates/spec.tt +13 -41
  82. data/lib/generators/rspec/templates/visual_spec.tt +20 -0
  83. data/lib/generators/template_renderer/partial_cache.rb +126 -0
  84. data/lib/generators/template_renderer/partial_resolver.rb +110 -0
  85. data/lib/generators/template_renderer/template_error.rb +50 -0
  86. data/lib/generators/template_renderer.rb +106 -0
  87. data/lib/generators/templates/common/config.tt +2 -2
  88. data/lib/generators/templates/common/gemfile.tt +36 -9
  89. data/lib/generators/templates/common/git_ignore.tt +6 -1
  90. data/lib/generators/templates/common/partials/mobile_config.tt +5 -1
  91. data/lib/generators/templates/common/partials/web_config.tt +17 -8
  92. data/lib/generators/templates/common/rakefile.tt +36 -0
  93. data/lib/generators/templates/common/read_me.tt +43 -91
  94. data/lib/generators/templates/common/rspec.tt +3 -0
  95. data/lib/generators/templates/common/ruby_version.tt +1 -0
  96. data/lib/generators/templates/helpers/allure_helper.tt +13 -2
  97. data/lib/generators/templates/helpers/browser_helper.tt +13 -2
  98. data/lib/generators/templates/helpers/capybara_helper.tt +32 -0
  99. data/lib/generators/templates/helpers/debug_helper.tt +190 -0
  100. data/lib/generators/templates/helpers/driver_helper.tt +3 -11
  101. data/lib/generators/templates/helpers/partials/allure_imports.tt +3 -1
  102. data/lib/generators/templates/helpers/partials/allure_requirements.tt +3 -1
  103. data/lib/generators/templates/helpers/partials/appium_driver.tt +44 -0
  104. data/lib/generators/templates/helpers/partials/browserstack_config.tt +13 -0
  105. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +7 -0
  106. data/lib/generators/templates/helpers/partials/debug_start.tt +7 -0
  107. data/lib/generators/templates/helpers/partials/driver_and_options.tt +5 -115
  108. data/lib/generators/templates/helpers/partials/quit_driver.tt +3 -1
  109. data/lib/generators/templates/helpers/partials/screenshot.tt +3 -1
  110. data/lib/generators/templates/helpers/partials/selenium_driver.tt +26 -0
  111. data/lib/generators/templates/helpers/partials/video_start.tt +9 -0
  112. data/lib/generators/templates/helpers/partials/video_stop.tt +4 -0
  113. data/lib/generators/templates/helpers/performance_helper.tt +57 -0
  114. data/lib/generators/templates/helpers/spec_helper.tt +72 -10
  115. data/lib/generators/templates/helpers/test_helper.tt +94 -0
  116. data/lib/generators/templates/helpers/video_helper.tt +270 -0
  117. data/lib/generators/templates/helpers/visual_helper.tt +39 -46
  118. data/lib/llm/client.rb +79 -0
  119. data/lib/llm/config.rb +57 -0
  120. data/lib/llm/prompts.rb +84 -0
  121. data/lib/llm/provider.rb +27 -0
  122. data/lib/llm/providers/anthropic_provider.rb +43 -0
  123. data/lib/llm/providers/ollama_provider.rb +56 -0
  124. data/lib/llm/providers/openai_provider.rb +42 -0
  125. data/lib/llm/response_parser.rb +67 -0
  126. data/lib/plugin/plugin.rb +22 -20
  127. data/lib/plugin/plugin_exposer.rb +16 -38
  128. data/lib/ruby_raider.rb +51 -11
  129. data/lib/scaffolding/crud_generator.rb +94 -0
  130. data/lib/scaffolding/dry_run_presenter.rb +16 -0
  131. data/lib/scaffolding/name_normalizer.rb +63 -0
  132. data/lib/scaffolding/page_introspector.rb +45 -0
  133. data/lib/scaffolding/project_detector.rb +72 -0
  134. data/lib/scaffolding/scaffold_menu.rb +103 -0
  135. data/lib/scaffolding/scaffolding.rb +158 -11
  136. data/lib/scaffolding/templates/component.tt +30 -0
  137. data/lib/scaffolding/templates/feature.tt +4 -4
  138. data/lib/scaffolding/templates/helper.tt +15 -1
  139. data/lib/scaffolding/templates/page_from_url.tt +75 -0
  140. data/lib/scaffolding/templates/page_object.tt +50 -1
  141. data/lib/scaffolding/templates/spec.tt +33 -2
  142. data/lib/scaffolding/templates/spec_from_page.tt +31 -0
  143. data/lib/scaffolding/templates/spec_from_url.tt +46 -0
  144. data/lib/scaffolding/templates/steps.tt +17 -5
  145. data/lib/scaffolding/url_analyzer.rb +179 -0
  146. data/lib/utilities/desktop_downloader.rb +177 -0
  147. data/lib/utilities/logo.rb +83 -0
  148. data/lib/utilities/utilities.rb +53 -20
  149. data/lib/version +1 -1
  150. data/ruby_raider.gemspec +1 -0
  151. data/sig/adopter/adopt_menu.rbs +25 -0
  152. data/sig/adopter/converters/base_converter.rbs +23 -0
  153. data/sig/adopter/converters/identity_converter.rbs +16 -0
  154. data/sig/adopter/migration_plan.rbs +34 -0
  155. data/sig/adopter/migrator.rbs +21 -0
  156. data/sig/adopter/plan_builder.rbs +38 -0
  157. data/sig/adopter/project_analyzer.rbs +39 -0
  158. data/sig/adopter/project_detector.rbs +26 -0
  159. data/sig/commands/adopt_commands.rbs +8 -0
  160. data/sig/commands/loaded_commands.rbs +5 -0
  161. data/sig/commands/plugin_commands.rbs +9 -0
  162. data/sig/commands/scaffolding_commands.rbs +28 -0
  163. data/sig/commands/utility_commands.rbs +21 -0
  164. data/sig/generators/automation/automation_generator.rbs +20 -0
  165. data/sig/generators/common_generator.rbs +12 -0
  166. data/sig/generators/cucumber/cucumber_generator.rbs +16 -0
  167. data/sig/generators/generator.rbs +40 -0
  168. data/sig/generators/helper_generator.rbs +18 -0
  169. data/sig/generators/infrastructure/github_generator.rbs +5 -0
  170. data/sig/generators/infrastructure/gitlab_generator.rbs +4 -0
  171. data/sig/generators/invoke_generators.rbs +10 -0
  172. data/sig/generators/menu_generator.rbs +29 -0
  173. data/sig/generators/minitest/minitest_generator.rbs +8 -0
  174. data/sig/generators/rspec/rspec_generator.rbs +8 -0
  175. data/sig/generators/template_renderer/partial_cache.rbs +20 -0
  176. data/sig/generators/template_renderer/partial_resolver.rbs +20 -0
  177. data/sig/generators/template_renderer/template_error.rbs +19 -0
  178. data/sig/generators/template_renderer.rbs +10 -0
  179. data/sig/llm/client.rbs +15 -0
  180. data/sig/llm/config.rbs +20 -0
  181. data/sig/llm/prompts.rbs +8 -0
  182. data/sig/llm/provider.rbs +12 -0
  183. data/sig/llm/providers/anthropic_provider.rbs +16 -0
  184. data/sig/llm/providers/ollama_provider.rbs +18 -0
  185. data/sig/llm/providers/openai_provider.rbs +16 -0
  186. data/sig/llm/response_parser.rbs +13 -0
  187. data/sig/plugin/plugin.rbs +24 -0
  188. data/sig/plugin/plugin_exposer.rbs +20 -0
  189. data/sig/ruby_raider.rbs +15 -0
  190. data/sig/scaffolding/crud_generator.rbs +16 -0
  191. data/sig/scaffolding/dry_run_presenter.rbs +4 -0
  192. data/sig/scaffolding/name_normalizer.rbs +17 -0
  193. data/sig/scaffolding/page_introspector.rbs +14 -0
  194. data/sig/scaffolding/project_detector.rbs +14 -0
  195. data/sig/scaffolding/scaffold_menu.rbs +18 -0
  196. data/sig/scaffolding/scaffolding.rbs +55 -0
  197. data/sig/scaffolding/url_analyzer.rbs +28 -0
  198. data/sig/utilities/desktop_downloader.rbs +23 -0
  199. data/sig/utilities/logger.rbs +13 -0
  200. data/sig/utilities/logo.rbs +16 -0
  201. data/sig/utilities/utilities.rbs +30 -0
  202. data/sig/vendor/thor.rbs +34 -0
  203. data/sig/vendor/tty_prompt.rbs +15 -0
  204. data/spec/adopter/adopt_menu_spec.rb +176 -0
  205. data/spec/adopter/converters/identity_converter_spec.rb +145 -0
  206. data/spec/adopter/migration_plan_spec.rb +113 -0
  207. data/spec/adopter/migrator_spec.rb +277 -0
  208. data/spec/adopter/plan_builder_spec.rb +298 -0
  209. data/spec/adopter/project_analyzer_spec.rb +337 -0
  210. data/spec/adopter/project_detector_spec.rb +295 -0
  211. data/spec/commands/raider_commands_spec.rb +129 -0
  212. data/spec/generators/fixtures/templates/test.tt +1 -0
  213. data/spec/generators/fixtures/templates/test_partial.tt +1 -0
  214. data/spec/generators/generator_spec.rb +23 -0
  215. data/spec/generators/template_renderer_spec.rb +298 -0
  216. data/spec/integration/commands/scaffolding_commands_spec.rb +2 -2
  217. data/spec/integration/commands/utility_commands_spec.rb +24 -4
  218. data/spec/integration/content/ci_content_spec.rb +119 -0
  219. data/spec/integration/content/common_content_spec.rb +288 -0
  220. data/spec/integration/content/config_content_spec.rb +175 -0
  221. data/spec/integration/content/content_helper.rb +32 -0
  222. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  223. data/spec/integration/content/helper_content_spec.rb +485 -0
  224. data/spec/integration/content/page_content_spec.rb +259 -0
  225. data/spec/integration/content/reporter_content_spec.rb +236 -0
  226. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  227. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  228. data/spec/integration/content/test_content_spec.rb +266 -0
  229. data/spec/integration/end_to_end_features_spec.rb +690 -0
  230. data/spec/integration/end_to_end_spec.rb +361 -0
  231. data/spec/integration/generators/automation_generator_spec.rb +9 -21
  232. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  233. data/spec/integration/generators/common_generator_spec.rb +48 -49
  234. data/spec/integration/generators/config_features_spec.rb +155 -0
  235. data/spec/integration/generators/cucumber_generator_spec.rb +7 -7
  236. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  237. data/spec/integration/generators/github_generator_spec.rb +8 -8
  238. data/spec/integration/generators/gitlab_generator_spec.rb +8 -8
  239. data/spec/integration/generators/helpers_generator_spec.rb +70 -44
  240. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  241. data/spec/integration/generators/minitest_generator_spec.rb +64 -0
  242. data/spec/integration/generators/reporter_spec.rb +159 -0
  243. data/spec/integration/generators/rspec_generator_spec.rb +7 -7
  244. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  245. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  246. data/spec/integration/settings_helper.rb +1 -4
  247. data/spec/integration/spec_helper.rb +46 -11
  248. data/spec/llm/client_spec.rb +79 -0
  249. data/spec/llm/config_spec.rb +92 -0
  250. data/spec/llm/prompts_spec.rb +49 -0
  251. data/spec/llm/response_parser_spec.rb +92 -0
  252. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  253. data/spec/menus/menu_generator_spec.rb +263 -0
  254. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  255. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  256. data/spec/scaffolding/scaffold_project_detector_spec.rb +104 -0
  257. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  258. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  259. data/spec/system/adopt_matrix_spec.rb +537 -0
  260. data/spec/system/adopt_spec.rb +225 -0
  261. data/spec/system/capybara_spec.rb +42 -0
  262. data/spec/system/selenium_spec.rb +19 -17
  263. data/spec/system/support/system_test_helper.rb +33 -0
  264. data/spec/system/watir_spec.rb +19 -17
  265. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  266. metadata +193 -18
  267. data/.github/workflows/push_gem.yml +0 -37
  268. data/.github/workflows/selenium.yml +0 -22
  269. data/.github/workflows/watir.yml +0 -22
  270. data/lib/generators/automation/templates/partials/android_caps.tt +0 -17
  271. data/lib/generators/automation/templates/partials/cross_platform_caps.tt +0 -25
  272. data/lib/generators/automation/templates/partials/ios_caps.tt +0 -18
  273. data/lib/generators/automation/templates/partials/selenium_account.tt +0 -9
  274. data/lib/generators/automation/templates/partials/selenium_login.tt +0 -34
  275. data/lib/generators/automation/templates/partials/watir_account.tt +0 -7
  276. data/lib/generators/automation/templates/partials/watir_login.tt +0 -32
  277. data/lib/generators/automation/templates/visual_options.tt +0 -16
  278. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../abstract/page'
4
+
5
+ class <%= page_class_name %> < Page
6
+ def url(_page)
7
+ '<%= url_data[:url_path] %>'
8
+ end
9
+
10
+ # Actions
11
+ <%- form_elements = url_data[:elements].select { |e| %i[input textarea select].include?(e[:type]) } -%>
12
+ <%- button_elements = url_data[:elements].select { |e| %i[button submit].include?(e[:type]) } -%>
13
+ <%- unless form_elements.empty? -%>
14
+
15
+ <%- if capybara? -%>
16
+ def fill_form(<%= form_elements.map { |e| e[:name] }.join(', ') %>)
17
+ <%- form_elements.each do |el| -%>
18
+ <%- if el[:locator][:type] == :id -%>
19
+ fill_in '<%= el[:locator][:value] %>', with: <%= el[:name] %>
20
+ <%- elsif el[:locator][:type] == :name -%>
21
+ fill_in '<%= el[:locator][:value] %>', with: <%= el[:name] %>
22
+ <%- else -%>
23
+ find('<%= el[:locator][:value] %>').set(<%= el[:name] %>)
24
+ <%- end -%>
25
+ <%- end -%>
26
+ end
27
+ <%- else -%>
28
+ def fill_form(<%= form_elements.map { |e| e[:name] }.join(', ') %>)
29
+ <%- form_elements.each do |el| -%>
30
+ <%= el[:name] %>_field.<%= selenium? ? 'send_keys' : 'set' %> <%= el[:name] %>
31
+ <%- end -%>
32
+ end
33
+ <%- end -%>
34
+ <%- end -%>
35
+ <%- unless button_elements.empty? -%>
36
+
37
+ <%- button_elements.each do |el| -%>
38
+ <%- if capybara? -%>
39
+ def click_<%= el[:name] %>
40
+ click_button '<%= el[:text] || el[:name] %>'
41
+ end
42
+ <%- else -%>
43
+ def click_<%= el[:name] %>
44
+ <%= el[:name] %>_button.click
45
+ end
46
+ <%- end -%>
47
+ <%- end -%>
48
+ <%- end -%>
49
+ <%- unless capybara? -%>
50
+
51
+ private
52
+
53
+ # Elements
54
+ <%- form_elements.each do |el| -%>
55
+
56
+ def <%= el[:name] %>_field
57
+ <%- if selenium? -%>
58
+ driver.find_element(<%= el[:locator][:type] %>: '<%= el[:locator][:value] %>')
59
+ <%- elsif watir? -%>
60
+ browser.<%= el[:type] == :select ? 'select' : 'text_field' %>(<%= el[:locator][:type] %>: '<%= el[:locator][:value] %>')
61
+ <%- end -%>
62
+ end
63
+ <%- end -%>
64
+ <%- button_elements.each do |el| -%>
65
+
66
+ def <%= el[:name] %>_button
67
+ <%- if selenium? -%>
68
+ driver.find_element(<%= el[:locator][:type] %>: '<%= el[:locator][:value] %>')
69
+ <%- elsif watir? -%>
70
+ browser.button(<%= el[:locator][:type] %>: '<%= el[:locator][:value] %>')
71
+ <%- end -%>
72
+ end
73
+ <%- end -%>
74
+ <%- end -%>
75
+ end
@@ -1,12 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../abstract/page'
4
+ <%- uses_list.each do |dep| -%>
5
+ require_relative '<%= dep %>'
6
+ <%- end -%>
7
+ <% if nested? -%>
8
+ <% module_parts.each_with_index do |mod, i| -%>
9
+ <%= ' ' * i %>module <%= mod %>
10
+ <% end -%>
11
+ <%= ' ' * module_parts.size %>class <%= NameNormalizer.to_class_name(leaf_name) %>Page < Page
12
+ <%- if project_config['url'] -%>
13
+ <%= ' ' * module_parts.size %> def url(_page)
14
+ <%= ' ' * module_parts.size %> '<%= normalized_name %>'
15
+ <%= ' ' * module_parts.size %> end
16
+ <%- end -%>
4
17
 
5
- class <%= name.split('_').map {|word| word.capitalize }.join %> < Page
18
+ <%= ' ' * module_parts.size %> # Actions
19
+ <%- if capybara? -%>
20
+
21
+ <%= ' ' * module_parts.size %> # Example: fill_in 'field_id', with: value
22
+ <%- else -%>
23
+
24
+ <%= ' ' * module_parts.size %> private
25
+
26
+ <%= ' ' * module_parts.size %> # Elements
27
+ <%- if selenium? -%>
28
+ <%= ' ' * module_parts.size %> # Example: driver.find_element(id: 'element_id')
29
+ <%- elsif watir? -%>
30
+ <%= ' ' * module_parts.size %> # Example: browser.text_field(id: 'element_id')
31
+ <%- end -%>
32
+ <%- end -%>
33
+ <%= ' ' * module_parts.size %>end
34
+ <% module_parts.each_with_index do |_mod, i| -%>
35
+ <%= ' ' * (module_parts.size - i - 1) %>end
36
+ <% end -%>
37
+ <% else -%>
38
+ class <%= page_class_name %> < Page
39
+ <%- if project_config['url'] -%>
40
+ def url(_page)
41
+ '<%= normalized_name %>'
42
+ end
43
+ <%- end -%>
6
44
 
7
45
  # Actions
46
+ <%- if capybara? -%>
47
+
48
+ # Example: fill_in 'field_id', with: value
49
+ <%- else -%>
8
50
 
9
51
  private
10
52
 
11
53
  # Elements
54
+ <%- if selenium? -%>
55
+ # Example: driver.find_element(id: 'element_id')
56
+ <%- elsif watir? -%>
57
+ # Example: browser.text_field(id: 'element_id')
58
+ <%- end -%>
59
+ <%- end -%>
12
60
  end
61
+ <% end -%>
@@ -1,4 +1,35 @@
1
- require_relative '../page_objects/pages/<%= name %>'
1
+ # frozen_string_literal: true
2
2
 
3
- describe '<%= name.split('_').map {|word| word.capitalize }.join %>' do
3
+ require_relative '../page_objects/pages/<%= normalized_name %>'
4
+ <%- uses_list.each do |dep| -%>
5
+ require_relative '../page_objects/pages/<%= dep %>'
6
+ <%- end -%>
7
+
8
+ describe '<%= page_class_name %>' do
9
+ <%- if selenium? -%>
10
+ let(:page) { <%= page_class_name %>.new(driver) }
11
+ <%- elsif watir? -%>
12
+ let(:page) { <%= page_class_name %>.new(browser) }
13
+ <%- elsif capybara? -%>
14
+ let(:page) { <%= page_class_name %>.new }
15
+ <%- end -%>
16
+ <%- uses_list.each do |dep| -%>
17
+ let(:<%= dep %>) { <%= NameNormalizer.to_page_class(dep) %>.new<%= selenium? ? '(driver)' : watir? ? '(browser)' : '' %> }
18
+ <%- end -%>
19
+ <%- if project_config['url'] -%>
20
+
21
+ before do
22
+ <%- if capybara? -%>
23
+ visit '<%= normalized_name %>'
24
+ <%- elsif selenium? -%>
25
+ driver.navigate.to "\#{project_config['url']}/<%= normalized_name %>"
26
+ <%- elsif watir? -%>
27
+ browser.goto "\#{project_config['url']}/<%= normalized_name %>"
28
+ <%- end -%>
29
+ end
30
+ <%- end -%>
31
+
32
+ it 'loads successfully' do
33
+ pending 'implement test'
34
+ end
4
35
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../page_objects/pages/<%= normalized_name %>'
4
+
5
+ describe '<%= introspected.class_name %>' do
6
+ <%- if selenium? -%>
7
+ let(:page) { <%= introspected.class_name %>.new(driver) }
8
+ <%- elsif watir? -%>
9
+ let(:page) { <%= introspected.class_name %>.new(browser) }
10
+ <%- elsif capybara? -%>
11
+ let(:page) { <%= introspected.class_name %>.new }
12
+ <%- end -%>
13
+ <%- introspected.methods.each do |method| -%>
14
+ <%- scenario = ai_scenarios[method[:name]] -%>
15
+
16
+ describe '#<%= method[:name] %>' do
17
+ it '<%= scenario ? scenario[:description] : method[:name].tr("_", " ") %>' do
18
+ <%- if method[:params].empty? -%>
19
+ page.<%= method[:name] %>
20
+ <%- else -%>
21
+ page.<%= method[:name] %>(<%= method[:params].map { |p| "'#{p}'" }.join(', ') %>)
22
+ <%- end -%>
23
+ <%- if scenario && scenario[:assertion_hint] -%>
24
+ pending '<%= scenario[:assertion_hint] %>'
25
+ <%- else -%>
26
+ pending 'add assertions'
27
+ <%- end -%>
28
+ end
29
+ end
30
+ <%- end -%>
31
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../page_objects/pages/<%= normalized_name %>'
4
+
5
+ describe '<%= page_class_name %>' do
6
+ <%- if selenium? -%>
7
+ let(:page) { <%= page_class_name %>.new(driver) }
8
+ <%- elsif watir? -%>
9
+ let(:page) { <%= page_class_name %>.new(browser) }
10
+ <%- elsif capybara? -%>
11
+ let(:page) { <%= page_class_name %>.new }
12
+ <%- end -%>
13
+
14
+ before do
15
+ <%- if capybara? -%>
16
+ visit '<%= url_data[:url_path] %>'
17
+ <%- elsif selenium? -%>
18
+ driver.navigate.to '<%= url_data[:url] %>'
19
+ <%- elsif watir? -%>
20
+ browser.goto '<%= url_data[:url] %>'
21
+ <%- end -%>
22
+ end
23
+ <%- form_elements = url_data[:elements].select { |e| %i[input textarea select].include?(e[:type]) } -%>
24
+ <%- unless form_elements.empty? -%>
25
+
26
+ describe '#fill_form' do
27
+ it 'fills in the form fields' do
28
+ page.fill_form(<%= form_elements.map { |e| "'test_#{e[:name]}'" }.join(', ') %>)
29
+ pending 'add assertions'
30
+ end
31
+ end
32
+ <%- end -%>
33
+ <%- url_data[:elements].select { |e| %i[button submit].include?(e[:type]) }.each do |el| -%>
34
+
35
+ describe '#click_<%= el[:name] %>' do
36
+ it 'clicks the <%= el[:name].tr("_", " ") %> button' do
37
+ page.click_<%= el[:name] %>
38
+ pending 'add assertions'
39
+ end
40
+ end
41
+ <%- end -%>
42
+
43
+ it 'loads the page' do
44
+ pending 'add assertions'
45
+ end
46
+ end
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../page_objects/pages/<%= name %>'
3
+ require_relative '../../page_objects/pages/<%= normalized_name %>'
4
+ <%- uses_list.each do |dep| -%>
5
+ require_relative '../../page_objects/pages/<%= dep %>'
6
+ <%- end -%>
4
7
 
5
- Given ('') do
8
+ Given('I am on the <%= normalized_name.tr("_", " ") %> page') do
9
+ <%- if selenium? -%>
10
+ @page = <%= page_class_name %>.new(driver)
11
+ <%- elsif watir? -%>
12
+ @page = <%= page_class_name %>.new(browser)
13
+ <%- elsif capybara? -%>
14
+ visit '<%= normalized_name %>'
15
+ <%- end -%>
6
16
  end
7
17
 
8
- When ('') do
18
+ When('I perform an action') do
19
+ pending 'implement step'
9
20
  end
10
21
 
11
- Then ('') do
12
- end
22
+ Then('I see the expected result') do
23
+ pending 'implement step'
24
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+
6
+ class UrlAnalyzer
7
+ INTERACTIVE_TAGS = %w[input select textarea button].freeze
8
+
9
+ attr_reader :url, :page_name, :elements
10
+
11
+ def initialize(url, name_override: nil, ai: false) # rubocop:disable Naming/MethodParameterName
12
+ @url = url
13
+ @uri = URI.parse(url)
14
+ @page_name = name_override || derive_page_name
15
+ @elements = []
16
+ @ai = ai
17
+ end
18
+
19
+ def analyze
20
+ html = fetch_html
21
+ if @ai
22
+ ai_elements = analyze_with_llm(html)
23
+ @elements = ai_elements if ai_elements
24
+ end
25
+ parse_elements(html) if @elements.empty?
26
+ self
27
+ end
28
+
29
+ def to_h
30
+ { page_name: @page_name, url: @url, url_path: @uri.path, elements: @elements }
31
+ end
32
+
33
+ private
34
+
35
+ def fetch_html
36
+ response = Net::HTTP.get_response(@uri)
37
+ response.body
38
+ rescue StandardError => e
39
+ raise "Failed to fetch #{@url}: #{e.message}"
40
+ end
41
+
42
+ def parse_elements(html)
43
+ # Simple regex-based HTML parsing (no external dependency needed)
44
+ parse_inputs(html)
45
+ parse_selects(html)
46
+ parse_textareas(html)
47
+ parse_buttons(html)
48
+ parse_links(html)
49
+ end
50
+
51
+ def parse_inputs(html)
52
+ html.scan(/<input\s+([^>]*)>/i).each do |match|
53
+ attrs = parse_attributes(match[0])
54
+ next if %w[hidden submit].include?(attrs['type'])
55
+
56
+ @elements << build_element(
57
+ name: attrs['name'] || attrs['id'] || attrs['placeholder'] || 'input',
58
+ type: :input,
59
+ input_type: attrs['type'] || 'text',
60
+ locator: best_locator(attrs)
61
+ )
62
+ end
63
+ end
64
+
65
+ def parse_selects(html)
66
+ html.scan(/<select\s+([^>]*)>/i).each do |match|
67
+ attrs = parse_attributes(match[0])
68
+ @elements << build_element(
69
+ name: attrs['name'] || attrs['id'] || 'select',
70
+ type: :select,
71
+ locator: best_locator(attrs)
72
+ )
73
+ end
74
+ end
75
+
76
+ def parse_textareas(html)
77
+ html.scan(/<textarea\s+([^>]*)>/i).each do |match|
78
+ attrs = parse_attributes(match[0])
79
+ @elements << build_element(
80
+ name: attrs['name'] || attrs['id'] || 'textarea',
81
+ type: :textarea,
82
+ locator: best_locator(attrs)
83
+ )
84
+ end
85
+ end
86
+
87
+ def parse_buttons(html)
88
+ html.scan(%r{<button\s+([^>]*)>([^<]*)</button>}i).each do |match|
89
+ attrs = parse_attributes(match[0])
90
+ text = match[1].strip
91
+ @elements << build_element(
92
+ name: attrs['id'] || attrs['name'] || text.downcase.gsub(/\s+/, '_') || 'button',
93
+ type: :button,
94
+ text:,
95
+ locator: best_locator(attrs, text:)
96
+ )
97
+ end
98
+
99
+ html.scan(/<input\s+([^>]*type=["']submit["'][^>]*)>/i).each do |match|
100
+ attrs = parse_attributes(match[0])
101
+ @elements << build_element(
102
+ name: attrs['id'] || attrs['name'] || attrs['value']&.downcase&.gsub(/\s+/, '_') || 'submit',
103
+ type: :submit,
104
+ text: attrs['value'] || 'Submit',
105
+ locator: best_locator(attrs)
106
+ )
107
+ end
108
+ end
109
+
110
+ def parse_links(html)
111
+ html.scan(%r{<a\s+([^>]*)>([^<]*)</a>}i).first(5)&.each do |match|
112
+ attrs = parse_attributes(match[0])
113
+ text = match[1].strip
114
+ next if text.empty?
115
+
116
+ @elements << build_element(
117
+ name: text.downcase.gsub(/\s+/, '_'),
118
+ type: :link,
119
+ text:,
120
+ locator: best_locator(attrs, text:)
121
+ )
122
+ end
123
+ end
124
+
125
+ def parse_attributes(attr_string)
126
+ attrs = {}
127
+ attr_string.scan(/([\w-]+)=["']([^"']*)["']/).each do |key, value|
128
+ attrs[key] = value
129
+ end
130
+ attrs
131
+ end
132
+
133
+ def best_locator(attrs, text: nil)
134
+ if attrs['id']
135
+ { type: :id, value: attrs['id'] }
136
+ elsif attrs['name']
137
+ { type: :name, value: attrs['name'] }
138
+ elsif attrs['class'] && !attrs['class'].empty?
139
+ { type: :css, value: ".#{attrs['class'].split.first}" }
140
+ elsif text && !text.empty?
141
+ { type: :xpath, value: "//#{infer_tag(attrs)}[contains(text(), '#{text}')]" }
142
+ else
143
+ { type: :css, value: attrs.first&.last || 'unknown' }
144
+ end
145
+ end
146
+
147
+ def infer_tag(attrs)
148
+ return 'button' if attrs['type'] == 'button' || attrs['type'] == 'submit'
149
+ return 'a' if attrs['href']
150
+
151
+ '*'
152
+ end
153
+
154
+ def build_element(name:, type:, locator:, **extra)
155
+ clean_name = name.to_s.gsub(/[^a-z0-9_]/i, '_').gsub(/_+/, '_').downcase.delete_prefix('_').delete_suffix('_')
156
+ { name: clean_name, type:, locator: }.merge(extra)
157
+ end
158
+
159
+ def analyze_with_llm(html)
160
+ require_relative '../llm/client'
161
+ require_relative '../llm/prompts'
162
+ require_relative '../llm/response_parser'
163
+
164
+ return nil unless Llm::Client.available?
165
+
166
+ response = Llm::Client.complete(
167
+ Llm::Prompts.analyze_page(html, @url),
168
+ system_prompt: Llm::Prompts.system_prompt
169
+ )
170
+ Llm::ResponseParser.extract_elements(response)
171
+ end
172
+
173
+ def derive_page_name
174
+ path = @uri.path.to_s.gsub(%r{^/|/$}, '')
175
+ return 'home' if path.empty?
176
+
177
+ path.split('/').last.gsub(/[^a-z0-9]/i, '_').downcase
178
+ end
179
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'rbconfig'
7
+
8
+ # Downloads the latest Raider Desktop release for the current platform.
9
+ # Uses the GitHub API to discover the latest release and match the right artifact.
10
+ module DesktopDownloader
11
+ REPO = 'RaiderHQ/raider_desktop'
12
+ API_URL = "https://api.github.com/repos/#{REPO}/releases/latest".freeze
13
+
14
+ PLATFORM_PATTERNS = {
15
+ 'mac_arm' => /\.dmg$/i,
16
+ 'mac_intel' => /\.dmg$/i,
17
+ 'windows' => /setup\.exe$/i,
18
+ 'linux_deb' => /\.deb$/i,
19
+ 'linux_appimage' => /\.AppImage$/i
20
+ }.freeze
21
+
22
+ HTTP_OPEN_TIMEOUT = 10
23
+ HTTP_READ_TIMEOUT = 15
24
+
25
+ class << self
26
+ def download(destination_dir = nil)
27
+ destination_dir ||= default_download_dir
28
+ asset = find_asset
29
+ unless asset
30
+ warn '[Ruby Raider] No desktop release found for your platform'
31
+ return nil
32
+ end
33
+
34
+ destination = File.join(destination_dir, asset[:name])
35
+ puts "Downloading Raider Desktop (#{asset[:name]})..."
36
+ download_file(asset[:url], destination)
37
+ puts "Downloaded to: #{destination}"
38
+ destination
39
+ end
40
+
41
+ def latest_version
42
+ cached_release&.dig('tag_name')
43
+ end
44
+
45
+ def download_url
46
+ asset = find_asset
47
+ asset&.dig(:url)
48
+ end
49
+
50
+ # Clear cached release data (useful between operations or for testing)
51
+ def clear_cache
52
+ @cached_release = :unset
53
+ end
54
+
55
+ def platform
56
+ host_os = RbConfig::CONFIG['host_os']
57
+ arch = RbConfig::CONFIG['host_cpu']
58
+
59
+ case host_os
60
+ when /darwin|mac os/i
61
+ arch =~ /arm|aarch64/i ? 'mac_arm' : 'mac_intel'
62
+ when /mswin|mingw|cygwin|windows/i
63
+ 'windows'
64
+ when /linux/i
65
+ 'linux_appimage'
66
+ else
67
+ 'linux_appimage'
68
+ end
69
+ end
70
+
71
+ def platform_display_name
72
+ case platform
73
+ when 'mac_arm' then 'macOS (Apple Silicon)'
74
+ when 'mac_intel' then 'macOS (Intel)'
75
+ when 'windows' then 'Windows'
76
+ when 'linux_deb' then 'Linux (deb)'
77
+ when 'linux_appimage' then 'Linux (AppImage)'
78
+ else 'Unknown'
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def find_asset
85
+ release = cached_release
86
+ return nil unless release
87
+
88
+ assets = release['assets'] || []
89
+ pattern = PLATFORM_PATTERNS[platform]
90
+ return nil unless pattern
91
+
92
+ matched = assets.find { |a| a['name'].match?(pattern) }
93
+ return nil unless matched
94
+
95
+ { name: matched['name'], url: matched['browser_download_url'], size: matched['size'] }
96
+ end
97
+
98
+ # Cache release data to avoid duplicate GitHub API calls
99
+ # (latest_version, find_asset, and download_url all need the same data)
100
+ def cached_release
101
+ return @cached_release if defined?(@cached_release) && @cached_release != :unset
102
+
103
+ @cached_release = fetch_latest_release
104
+ end
105
+
106
+ def fetch_latest_release
107
+ uri = URI(API_URL)
108
+ http = Net::HTTP.new(uri.host, uri.port)
109
+ http.use_ssl = uri.scheme == 'https'
110
+ http.open_timeout = HTTP_OPEN_TIMEOUT
111
+ http.read_timeout = HTTP_READ_TIMEOUT
112
+ response = http.get(uri.request_uri)
113
+ return nil unless response.is_a?(Net::HTTPSuccess)
114
+
115
+ JSON.parse(response.body)
116
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
117
+ warn "[Ruby Raider] GitHub API timed out: #{e.message}"
118
+ nil
119
+ rescue StandardError => e
120
+ warn "[Ruby Raider] Failed to check releases: #{e.message}"
121
+ nil
122
+ end
123
+
124
+ def download_file(url, destination)
125
+ FileUtils.mkdir_p(File.dirname(destination))
126
+ uri = URI(url)
127
+ follow_redirects(uri, destination)
128
+ end
129
+
130
+ def follow_redirects(uri, destination, limit = 5)
131
+ raise 'Too many redirects' if limit.zero?
132
+
133
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
134
+ open_timeout: HTTP_OPEN_TIMEOUT, read_timeout: 60) do |http|
135
+ request = Net::HTTP::Get.new(uri)
136
+ http.request(request) do |response|
137
+ case response
138
+ when Net::HTTPRedirection
139
+ follow_redirects(URI(response['location']), destination, limit - 1)
140
+ when Net::HTTPSuccess
141
+ write_response(response, destination)
142
+ else
143
+ raise "Download failed: #{response.code} #{response.message}"
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ def write_response(response, destination)
150
+ total = response['content-length']&.to_i
151
+ downloaded = 0
152
+ File.open(destination, 'wb') do |file|
153
+ response.read_body do |chunk|
154
+ file.write(chunk)
155
+ downloaded += chunk.size
156
+ print_progress(downloaded, total) if total&.positive?
157
+ end
158
+ end
159
+ puts
160
+ end
161
+
162
+ def print_progress(downloaded, total)
163
+ percent = (downloaded * 100.0 / total).round(1)
164
+ bar_width = 30
165
+ filled = (percent / 100.0 * bar_width).round
166
+ bar = "#{'=' * filled}#{' ' * (bar_width - filled)}"
167
+ mb = (downloaded / 1_048_576.0).round(1)
168
+ total_mb = (total / 1_048_576.0).round(1)
169
+ print "\r [#{bar}] #{percent}% (#{mb}/#{total_mb} MB)"
170
+ end
171
+
172
+ def default_download_dir
173
+ downloads = File.expand_path('~/Downloads')
174
+ File.directory?(downloads) ? downloads : Dir.pwd
175
+ end
176
+ end
177
+ end