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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/string/inflections'
3
4
  require 'yaml'
4
5
  require 'watir'
5
6
 
@@ -16,7 +17,17 @@ module BrowserHelper
16
17
  end
17
18
 
18
19
  def create_browser(*args)
19
- args = args.empty? ? config['browser_arguments'][config['browser']] : args
20
- Watir::Browser.new(config['browser'], options: { args: args })
20
+ Watir.default_timeout = config.fetch('timeout', 10)
21
+ browser_name = config['browser'].to_s
22
+ args = args.empty? ? config['browser_arguments'][browser_name] : args
23
+ args += ['--headless'] if ENV['HEADLESS'] && !args.include?('--headless')
24
+ capitalized = browser_name == 'ie' ? browser_name.upcase : browser_name.capitalize
25
+ browser_options = "Selenium::WebDriver::#{capitalized}::Options".constantize.new(args:)
26
+ debug_cfg = config['debug'] || {}
27
+ if %w[chrome edge msedge].include?(browser_name.downcase) &&
28
+ (debug_cfg.fetch('enabled', false) || ENV['DEBUG']&.downcase == 'true')
29
+ browser_options.add_option('goog:loggingPrefs', { browser: 'ALL', performance: 'ALL' })
30
+ end
31
+ Watir::Browser.new(browser_name.to_sym, options: browser_options)
21
32
  end
22
33
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara'
4
+ require 'capybara/dsl'
5
+ require 'selenium-webdriver'
6
+ require 'yaml'
7
+
8
+ module CapybaraHelper
9
+ def self.configure
10
+ config = YAML.load_file('config/config.yml')
11
+
12
+ Capybara.configure do |capybara_config|
13
+ capybara_config.default_driver = :selenium
14
+ capybara_config.javascript_driver = :selenium
15
+ capybara_config.app_host = config['url']
16
+ capybara_config.default_max_wait_time = config.fetch('timeout', 10)
17
+ capybara_config.run_server = false
18
+ end
19
+
20
+ Capybara.register_driver :selenium do |app|
21
+ browser = config['browser'].to_sym
22
+ args = config['browser_arguments'][config['browser']] || []
23
+ args += ['--headless'] if ENV['HEADLESS'] && !args.include?('--headless')
24
+ options = Selenium::WebDriver::Chrome::Options.new(args:)
25
+ debug_cfg = config['debug'] || {}
26
+ if debug_cfg.fetch('enabled', false) || ENV['DEBUG']&.downcase == 'true'
27
+ options.add_option('goog:loggingPrefs', { browser: 'ALL', performance: 'ALL' })
28
+ end
29
+ Capybara::Selenium::Driver.new(app, browser:, options:)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require 'logger'
6
+ require 'json'
7
+
8
+ module DebugHelper
9
+ LOG_DIR = 'debug_logs'
10
+
11
+ module_function
12
+
13
+ # --- Configuration ---
14
+
15
+ def config
16
+ @config ||= begin
17
+ yml = YAML.load_file('config/config.yml')
18
+ yml['debug'] || {}
19
+ rescue StandardError
20
+ {}
21
+ end
22
+ end
23
+
24
+ def enabled?
25
+ ENV['DEBUG']&.downcase == 'true' || config.fetch('enabled', false)
26
+ end
27
+
28
+ def console_logs?
29
+ enabled? && config.fetch('console_logs', true)
30
+ end
31
+
32
+ def action_logging?
33
+ enabled? && config.fetch('action_logging', true)
34
+ end
35
+
36
+ def network_logging?
37
+ enabled? && config.fetch('network_logging', true)
38
+ end
39
+
40
+ def log_dir
41
+ config.fetch('log_dir', LOG_DIR)
42
+ end
43
+
44
+ def ensure_log_dir
45
+ FileUtils.mkdir_p(log_dir)
46
+ end
47
+
48
+ # --- Driver Resolution ---
49
+
50
+ def resolve_selenium_driver(obj)
51
+ if obj.respond_to?(:driver) && !obj.is_a?(Selenium::WebDriver::Driver)
52
+ obj.driver
53
+ elsif obj.respond_to?(:browser) && obj.browser.respond_to?(:driver)
54
+ obj.browser.driver
55
+ else
56
+ obj
57
+ end
58
+ rescue StandardError
59
+ obj
60
+ end
61
+
62
+ # --- Failure Diagnostics (returns structured hash for desktop integration) ---
63
+
64
+ def capture_failure_diagnostics(driver_or_browser, test_name, exception: nil)
65
+ return {} unless enabled?
66
+
67
+ ensure_log_dir
68
+ sel_driver = resolve_selenium_driver(driver_or_browser)
69
+ safe_name = sanitize(test_name)
70
+ diagnostics = { test_name: test_name, timestamp: Time.now.iso8601 }
71
+
72
+ diagnostics[:url] = sel_driver.current_url rescue 'unknown'
73
+ diagnostics[:title] = sel_driver.title rescue 'unknown'
74
+
75
+ if exception
76
+ diagnostics[:error_message] = exception.message
77
+ diagnostics[:error_class] = exception.class.name
78
+ diagnostics[:stack_trace] = exception.backtrace&.first(20) || []
79
+ end
80
+
81
+ diagnostics[:console_logs] = capture_console_logs(sel_driver) if console_logs?
82
+ diagnostics[:network_log_path] = capture_network_logs(sel_driver, safe_name) if network_logging?
83
+
84
+ # HTML snapshot
85
+ html_path = File.join(log_dir, "#{safe_name}_page.html")
86
+ File.write(html_path, sel_driver.page_source) rescue nil
87
+ diagnostics[:html_snapshot] = html_path
88
+
89
+ # Write diagnostics JSON (desktop app reads this)
90
+ summary_path = File.join(log_dir, "#{safe_name}_diagnostics.json")
91
+ File.write(summary_path, JSON.pretty_generate(diagnostics))
92
+ diagnostics[:summary_path] = summary_path
93
+
94
+ diagnostics
95
+ rescue StandardError => e
96
+ warn "[debug] Failed to capture diagnostics: #{e.message}"
97
+ {}
98
+ end
99
+
100
+ # --- Console Log Capture ---
101
+
102
+ def capture_console_logs(sel_driver)
103
+ return [] unless chrome_or_edge?(sel_driver)
104
+
105
+ sel_driver.logs.get(:browser).map { |entry| { level: entry.level, message: entry.message, timestamp: entry.timestamp } }
106
+ rescue StandardError
107
+ []
108
+ end
109
+
110
+ # --- Action Logger ---
111
+
112
+ class ActionLogger
113
+ def initialize(test_name)
114
+ DebugHelper.ensure_log_dir
115
+ log_path = File.join(DebugHelper.log_dir, "#{DebugHelper.sanitize(test_name)}_actions.log")
116
+ @logger = Logger.new(log_path)
117
+ @logger.formatter = proc { |severity, datetime, _progname, msg|
118
+ "[#{datetime.strftime('%H:%M:%S.%L')}] #{severity}: #{msg}\n"
119
+ }
120
+ end
121
+
122
+ def log(action, details = '')
123
+ @logger.info("#{action} #{details}".strip)
124
+ end
125
+
126
+ def close
127
+ @logger.close
128
+ rescue StandardError
129
+ nil
130
+ end
131
+ end
132
+
133
+ class NullActionLogger
134
+ def log(_action, _details = '') = nil
135
+ def close = nil
136
+ end
137
+
138
+ def action_logger_for(test_name)
139
+ action_logging? ? ActionLogger.new(test_name) : NullActionLogger.new
140
+ end
141
+
142
+ # --- Network Logging Setup ---
143
+
144
+ def enable_network_logging(driver_or_browser)
145
+ return unless network_logging?
146
+
147
+ sel_driver = resolve_selenium_driver(driver_or_browser)
148
+ return unless chrome_or_edge?(sel_driver)
149
+
150
+ sel_driver.execute_cdp('Network.enable')
151
+ rescue StandardError => e
152
+ warn "[debug] Could not enable network logging: #{e.message}"
153
+ end
154
+
155
+ # --- Network Log Capture ---
156
+
157
+ def capture_network_logs(sel_driver, safe_name)
158
+ return unless chrome_or_edge?(sel_driver)
159
+
160
+ logs = sel_driver.logs.get(:performance)
161
+ return if logs.empty?
162
+
163
+ entries = logs.filter_map do |entry|
164
+ parsed = JSON.parse(entry.message)['message'] rescue next
165
+ parsed if parsed.is_a?(Hash) && parsed['method']&.start_with?('Network.')
166
+ end
167
+
168
+ return if entries.empty?
169
+
170
+ network_path = File.join(log_dir, "#{safe_name}_network.json")
171
+ File.write(network_path, JSON.pretty_generate(entries))
172
+ network_path
173
+ rescue StandardError => e
174
+ warn "[debug] Failed to capture network logs: #{e.message}"
175
+ nil
176
+ end
177
+
178
+ # --- Helpers ---
179
+
180
+ def chrome_or_edge?(driver)
181
+ browser_name = driver.capabilities[:browser_name].to_s.downcase
182
+ %w[chrome chromium msedge edge].any? { |b| browser_name.include?(b) }
183
+ rescue StandardError
184
+ false
185
+ end
186
+
187
+ def sanitize(name)
188
+ name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')[0..80]
189
+ end
190
+ end
@@ -1,26 +1,18 @@
1
1
  # frozen_string_literal: true
2
- <% if axe? -%>
3
- require 'axe-selenium'
4
- <% end -%>
2
+
5
3
  require 'yaml'
6
- <%- unless axe? %>
7
4
  <%- if selenium_based? -%>
8
5
  require 'active_support/inflector'
9
6
  require 'selenium-webdriver'
10
7
  <%- else -%>
11
8
  require 'appium_lib'
12
9
  <%- end -%>
13
- <%- end -%>
14
10
 
15
11
  module DriverHelper
16
- <%- if selenium_based? && !axe? -%>
12
+ <%- if selenium_based? -%>
17
13
  def driver(*opts)
18
14
  @driver ||= create_driver(*opts)
19
15
  end
20
- <%- elsif axe? -%>
21
- def driver(browser = :chrome, js_path = nil, skip_iframes = nil)
22
- @driver ||= create_driver(browser, js_path, skip_iframes)
23
- end
24
16
  <%- else -%>
25
17
  def driver
26
18
  @driver ||= create_driver
@@ -29,5 +21,5 @@ module DriverHelper
29
21
 
30
22
  private
31
23
 
32
- <%= ERB.new(File.read(File.expand_path('./partials/driver_and_options.tt', __dir__)), trim_mode: '-').result(binding).strip! %>
24
+ <%= partial('driver_and_options', strip: true) %>
33
25
  end
@@ -1,5 +1,7 @@
1
1
  <% if rspec? %>
2
2
  AllureRspec
3
- <% else %>
3
+ <% elsif cucumber? %>
4
4
  AllureCucumber
5
+ <% else %>
6
+ Allure
5
7
  <% end %>
@@ -1,6 +1,8 @@
1
1
  <% if cucumber? %>
2
2
  require 'allure-cucumber'
3
- <% else %>
3
+ <% elsif rspec? %>
4
4
  require 'allure-ruby-commons'
5
5
  require 'allure-rspec'
6
+ <% else %>
7
+ require 'allure-ruby-commons'
6
8
  <% end %>
@@ -0,0 +1,44 @@
1
+ def create_driver
2
+ @driver = configure_driver
3
+ end
4
+ <%- if cross_platform? -%>
5
+
6
+ def platform
7
+ @platform ||= YAML.load_file('config/config.yml')['platform'].to_s
8
+ end
9
+
10
+ def platform_caps
11
+ @platform_caps ||= YAML.load_file('config/capabilities.yml')[platform]
12
+ end
13
+
14
+ def parsed_caps
15
+ platform_caps['appium:options']['app'] = parse_app_path(platform_caps['appium:options']['app'])
16
+ platform_caps
17
+ end
18
+
19
+ def parse_app_path(path)
20
+ File.expand_path(path, Dir.pwd)
21
+ end
22
+ <%- else -%>
23
+
24
+ def parsed_caps
25
+ caps = YAML.load_file('config/capabilities.yml')
26
+ caps['appium:options']['app'] = app_path(caps['appium:options']['app'])
27
+ caps
28
+ end
29
+
30
+ def app_path(path)
31
+ File.expand_path(path, Dir.pwd)
32
+ end
33
+ <%- end -%>
34
+
35
+ <%= partial('browserstack_config', strip: true) %>
36
+
37
+ def configure_driver
38
+ if browserstack?
39
+ Appium::Driver.new({ caps: browserstack_caps,
40
+ 'appium_lib': { server_url: parsed_browserstack_url}}, true)
41
+ else
42
+ Appium::Driver.new({ caps: parsed_caps })
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ def browserstack?
2
+ ENV['TEST_ENV'] == 'browserstack'
3
+ end
4
+
5
+ def browserstack_caps
6
+ @browserstack_caps ||= YAML.load_file('config/capabilities.yml')['browserstack']
7
+ end
8
+
9
+ def parsed_browserstack_url
10
+ username = ENV['BROWSERSTACK_USER']
11
+ access_key = ENV['BROWSERSTACK_KEY']
12
+ "https://#{username}:#{access_key}@hub-cloud.browserstack.com/wd/hub"
13
+ end
@@ -0,0 +1,7 @@
1
+ <% if capybara? %>
2
+ DebugHelper.capture_failure_diagnostics(Capybara.current_session.driver, example_name, exception: @_exception)
3
+ <% elsif selenium_based? %>
4
+ DebugHelper.capture_failure_diagnostics(driver, example_name, exception: @_exception)
5
+ <% elsif watir? %>
6
+ DebugHelper.capture_failure_diagnostics(browser, example_name, exception: @_exception)
7
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <% if capybara? %>
2
+ DebugHelper.enable_network_logging(Capybara.current_session.driver)
3
+ <% elsif selenium_based? %>
4
+ DebugHelper.enable_network_logging(driver)
5
+ <% elsif watir? %>
6
+ DebugHelper.enable_network_logging(browser)
7
+ <% end %>
@@ -1,115 +1,5 @@
1
- <% if axe? -%>
2
- def create_driver(browser, js_path, skip_iframes)
3
- AxeSelenium.configure(browser) do |config|
4
- config.jslib_path = js_path if js_path
5
- config.skip_iframes = skip_iframes if skip_iframes
6
- end.page
7
- end
8
- <% elsif selenium_based? -%>
9
- def create_driver(*opts)
10
- @config = YAML.load_file('config/config.yml')
11
- browser = @config['browser']
12
- Selenium::WebDriver.for(browser.to_sym, options: create_webdriver_options(*opts))
13
- end
14
-
15
- def browser_arguments(*opts)
16
- opts.empty? ? @config['browser_arguments'][@config['browser']] : opts
17
- end
18
-
19
- def driver_options
20
- @config['driver_options']
21
- end
22
-
23
- # :reek:FeatureEnvy
24
- def create_webdriver_options(*opts)
25
- load_browser = @config['browser'].to_s
26
- browser = load_browser == 'ie' ? load_browser.upcase : load_browser.capitalize
27
- options = "Selenium::WebDriver::#{browser}::Options".constantize.new
28
- browser_arguments(*opts).each { |arg| options.add_argument(arg) }
29
- driver_options.each { |opt| options.add_option(opt.first, opt.last) }
30
- options
31
- end
32
- <% elsif cross_platform? -%>
33
- def create_driver
34
- @driver = configure_driver
35
- end
36
-
37
- def platform
38
- @platform ||= YAML.load_file('config/config.yml')['platform'].to_s
39
- end
40
-
41
- def platform_caps
42
- @platform_caps ||= YAML.load_file('config/capabilities.yml')[platform]
43
- end
44
-
45
- # :reek:UtilityFunction
46
- def parsed_caps
47
- platform_caps['appium:options']['app'] = parse_app_path(platform_caps['appium:options']['app'])
48
- platform_caps
49
- end
50
-
51
- def parse_app_path(path)
52
- File.expand_path(path, Dir.pwd)
53
- end
54
-
55
- def browserstack?
56
- ENV['TEST_ENV'] == 'browserstack'
57
- end
58
-
59
- def browserstack_caps
60
- @browserstack_caps ||= YAML.load_file('config/capabilities.yml')['browserstack']
61
- end
62
-
63
- def configure_driver
64
- if browserstack?
65
- Appium::Driver.new({ caps: browserstack_caps,
66
- 'appium_lib': { server_url: parsed_browserstack_url}}, true)
67
- else
68
- Appium::Driver.new({ caps: parsed_caps })
69
- end
70
- end
71
-
72
- def parsed_browserstack_url
73
- username = ENV['BROWSERSTACK_USER']
74
- access_key = ENV['BROWSERSTACK_KEY']
75
- "https://#{username}:#{access_key}@hub-cloud.browserstack.com/wd/hub"
76
- end
77
- <% else -%>
78
- def create_driver
79
- @driver = configure_driver
80
- end
81
-
82
- # :reek:UtilityFunction
83
- def parsed_caps
84
- caps = YAML.load_file('config/capabilities.yml')
85
- caps['appium:options']['app'] = app_path(caps['appium:options']['app'])
86
- caps
87
- end
88
-
89
- def app_path(path)
90
- File.expand_path(path, Dir.pwd)
91
- end
92
-
93
- def browserstack?
94
- ENV['TEST_ENV'] == 'browserstack'
95
- end
96
-
97
- def browserstack_caps
98
- @browserstack_caps ||= YAML.load_file('config/capabilities.yml')['browserstack']
99
- end
100
-
101
- def configure_driver
102
- if browserstack?
103
- Appium::Driver.new({ caps: browserstack_caps,
104
- 'appium_lib': { server_url: parsed_browserstack_url}}, true)
105
- else
106
- Appium::Driver.new({ caps: parsed_caps })
107
- end
108
- end
109
-
110
- def parsed_browserstack_url
111
- username = ENV['BROWSERSTACK_USER']
112
- access_key = ENV['BROWSERSTACK_KEY']
113
- "https://#{username}:#{access_key}@hub-cloud.browserstack.com/wd/hub"
114
- end
115
- <% end -%>
1
+ <% if selenium_based? -%>
2
+ <%= partial('selenium_driver', strip: true) %>
3
+ <%- else -%>
4
+ <%= partial('appium_driver', strip: true) %>
5
+ <%- end -%>
@@ -1,4 +1,6 @@
1
- <% if selenium_based? %>
1
+ <% if capybara? %>
2
+ Capybara.reset_sessions!
3
+ <% elsif selenium_based? %>
2
4
  driver.quit
3
5
  <% elsif watir? %>
4
6
  browser.quit
@@ -1,4 +1,6 @@
1
- <% if selenium_based? %>
1
+ <% if capybara? %>
2
+ screenshot = Capybara.page.save_screenshot("#{temp_folder}/#{example_name}.png")
3
+ <% elsif selenium_based? %>
2
4
  screenshot = driver.save_screenshot("#{temp_folder}/#{example_name}.png")
3
5
  <% elsif watir? %>
4
6
  screenshot = browser.screenshot.save("#{temp_folder}/#{example_name}.png")
@@ -0,0 +1,26 @@
1
+ def create_driver(*opts)
2
+ @config = YAML.load_file('config/config.yml')
3
+ browser = @config['browser']
4
+ driver = Selenium::WebDriver.for(browser.to_sym, options: create_webdriver_options(*opts))
5
+ driver.manage.timeouts.implicit_wait = @config.fetch('timeout', 10)
6
+ driver
7
+ end
8
+
9
+ def browser_arguments(*opts)
10
+ args = opts.empty? ? @config['browser_arguments'][@config['browser']] : opts
11
+ args += ['--headless'] if ENV['HEADLESS'] && !args.include?('--headless')
12
+ args
13
+ end
14
+
15
+ def create_webdriver_options(*opts)
16
+ load_browser = @config['browser'].to_s
17
+ browser = load_browser == 'ie' ? load_browser.upcase : load_browser.capitalize
18
+ options = "Selenium::WebDriver::#{browser}::Options".constantize.new
19
+ browser_arguments(*opts).each { |arg| options.add_argument(arg) }
20
+ debug_cfg = @config['debug'] || {}
21
+ if %w[chrome edge msedge].include?(@config['browser'].to_s.downcase) &&
22
+ (debug_cfg.fetch('enabled', false) || ENV['DEBUG']&.downcase == 'true')
23
+ options.add_option('goog:loggingPrefs', { browser: 'ALL', performance: 'ALL' })
24
+ end
25
+ options
26
+ end
@@ -0,0 +1,9 @@
1
+ <% if capybara? %>
2
+ @video_recorder = VideoHelper.recorder_for(Capybara.current_session.driver)
3
+ <% elsif selenium_based? %>
4
+ @video_recorder = VideoHelper.recorder_for(driver)
5
+ <% elsif watir? %>
6
+ @video_recorder = VideoHelper.recorder_for(browser)
7
+ <% else %>
8
+ @video_recorder = VideoHelper.recorder_for(driver)
9
+ <% end %>
@@ -0,0 +1,4 @@
1
+ video_file = @video_recorder&.stop
2
+ <% if allure_reporter? %>
3
+ AllureHelper.add_video(example_name, video_file)
4
+ <% end %>
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+ require 'fileutils'
6
+ require 'yaml'
7
+
8
+ module PerformanceHelper
9
+ REPORTS_DIR = 'lighthouse_reports'
10
+
11
+ def run_lighthouse_audit(url, categories: %w[performance accessibility best-practices seo])
12
+ FileUtils.mkdir_p(REPORTS_DIR)
13
+
14
+ report_path = File.join(REPORTS_DIR, "report_#{Time.now.to_i}.json")
15
+ category_flags = categories.map { |c| "--only-categories=#{c}" }.join(' ')
16
+
17
+ config_timeout = if File.exist?('config/config.yml')
18
+ YAML.load_file('config/config.yml').fetch('timeout', 10)
19
+ else
20
+ 10
21
+ end
22
+ max_wait = config_timeout * 1000
23
+
24
+ command = "lighthouse #{url} --output=json --output-path=#{report_path} " \
25
+ "--chrome-flags=\"--headless --no-sandbox\" #{category_flags} " \
26
+ "--max-wait-for-load=#{max_wait} --quiet"
27
+
28
+ stdout, stderr, status = Open3.capture3(command)
29
+
30
+ unless status.success?
31
+ return { status: :error, message: "Lighthouse failed: #{stderr.strip.empty? ? stdout : stderr}" }
32
+ end
33
+
34
+ report = JSON.parse(File.read(report_path))
35
+ scores = extract_scores(report)
36
+
37
+ { status: :success, scores:, report_path: }
38
+ end
39
+
40
+ def assert_performance_above(url, threshold: 0.8, categories: %w[performance])
41
+ result = run_lighthouse_audit(url, categories:)
42
+ return result if result[:status] == :error
43
+
44
+ passed = result[:scores].all? { |_category, score| score >= threshold }
45
+
46
+ { status: passed ? :pass : :fail, scores: result[:scores], passed:, threshold:,
47
+ report_path: result[:report_path] }
48
+ end
49
+
50
+ private
51
+
52
+ def extract_scores(report)
53
+ report.fetch('categories', {}).each_with_object({}) do |(key, data), scores|
54
+ scores[key] = data['score']
55
+ end
56
+ end
57
+ end