ruby_raider 3.0.1 → 3.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/e2e_tests.yml +0 -17
- data/.github/workflows/system_tests.yml +0 -22
- data/README.md +8 -63
- data/lib/commands/scaffolding_commands.rb +1 -192
- data/lib/commands/utility_commands.rb +0 -52
- data/lib/generators/automation/templates/login.tt +1 -9
- data/lib/generators/automation/templates/page.tt +1 -7
- data/lib/generators/automation/templates/partials/initialize_selector.tt +1 -3
- data/lib/generators/automation/templates/partials/visit_method.tt +2 -6
- data/lib/generators/common_generator.rb +0 -6
- data/lib/generators/cucumber/cucumber_generator.rb +0 -24
- data/lib/generators/cucumber/templates/accessibility_steps.tt +2 -6
- data/lib/generators/cucumber/templates/cucumber.tt +0 -7
- data/lib/generators/cucumber/templates/env.tt +1 -3
- data/lib/generators/cucumber/templates/partials/appium_env.tt +1 -6
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -13
- data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -13
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -4
- data/lib/generators/cucumber/templates/world.tt +1 -3
- data/lib/generators/generator.rb +2 -46
- data/lib/generators/helper_generator.rb +4 -49
- data/lib/generators/infrastructure/templates/github.tt +2 -2
- data/lib/generators/infrastructure/templates/github_appium.tt +2 -6
- data/lib/generators/invoke_generators.rb +4 -25
- data/lib/generators/menu_generator.rb +10 -100
- data/lib/generators/rspec/rspec_generator.rb +0 -11
- data/lib/generators/rspec/templates/accessibility_spec.tt +2 -6
- data/lib/generators/rspec/templates/spec.tt +6 -8
- data/lib/generators/templates/common/gemfile.tt +0 -26
- data/lib/generators/templates/common/git_ignore.tt +0 -2
- data/lib/generators/templates/common/partials/mobile_config.tt +0 -4
- data/lib/generators/templates/common/partials/web_config.tt +1 -12
- data/lib/generators/templates/common/rakefile.tt +0 -9
- data/lib/generators/templates/common/read_me.tt +4 -10
- data/lib/generators/templates/helpers/allure_helper.tt +0 -10
- data/lib/generators/templates/helpers/browser_helper.tt +0 -5
- data/lib/generators/templates/helpers/partials/quit_driver.tt +1 -3
- data/lib/generators/templates/helpers/partials/screenshot.tt +1 -3
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +0 -5
- data/lib/generators/templates/helpers/spec_helper.tt +2 -44
- data/lib/generators/templates/helpers/test_helper.tt +0 -7
- data/lib/ruby_raider.rb +2 -50
- data/lib/scaffolding/project_detector.rb +2 -9
- data/lib/scaffolding/scaffolding.rb +0 -109
- data/lib/scaffolding/templates/component.tt +1 -4
- data/lib/scaffolding/templates/page_object.tt +0 -10
- data/lib/scaffolding/templates/spec.tt +2 -6
- data/lib/scaffolding/templates/steps.tt +0 -2
- data/lib/utilities/utilities.rb +20 -33
- data/lib/version +1 -1
- data/sig/commands/scaffolding_commands.rbs +0 -12
- data/sig/commands/utility_commands.rbs +0 -7
- data/sig/generators/cucumber/cucumber_generator.rbs +0 -4
- data/sig/generators/generator.rbs +0 -11
- data/sig/generators/helper_generator.rbs +1 -6
- data/sig/generators/invoke_generators.rbs +1 -2
- data/sig/generators/menu_generator.rbs +2 -6
- data/sig/generators/rspec/rspec_generator.rbs +0 -2
- data/sig/ruby_raider.rbs +1 -3
- data/sig/scaffolding/project_detector.rbs +0 -1
- data/sig/scaffolding/scaffolding.rbs +0 -23
- data/sig/utilities/utilities.rbs +0 -5
- data/spec/commands/raider_commands_spec.rb +0 -61
- data/spec/integration/commands/browser_update_after_creation_spec.rb +123 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +0 -20
- data/spec/integration/commands/utility_commands_spec.rb +5 -5
- data/spec/integration/content/ci_content_spec.rb +6 -61
- data/spec/integration/content/common_content_spec.rb +0 -64
- data/spec/integration/content/config_content_spec.rb +0 -86
- data/spec/integration/content/content_helper.rb +2 -2
- data/spec/integration/content/gemfile_content_spec.rb +0 -71
- data/spec/integration/content/helper_content_spec.rb +2 -284
- data/spec/integration/content/page_content_spec.rb +0 -89
- data/spec/integration/content/syntax_validation_spec.rb +2 -2
- data/spec/integration/content/test_content_spec.rb +0 -119
- data/spec/integration/end_to_end_features_spec.rb +13 -435
- data/spec/integration/end_to_end_spec.rb +0 -96
- data/spec/integration/generators/axe_addon_spec.rb +2 -52
- data/spec/integration/generators/common_generator_spec.rb +1 -13
- data/spec/integration/generators/config_features_spec.rb +3 -81
- data/spec/integration/generators/github_generator_spec.rb +5 -15
- data/spec/integration/generators/helpers_generator_spec.rb +0 -37
- data/spec/integration/scaffolding_e2e_spec.rb +2 -237
- data/spec/integration/settings_helper.rb +1 -3
- data/spec/integration/spec_helper.rb +6 -11
- data/spec/menus/menu_generator_spec.rb +4 -107
- data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -26
- data/spec/scaffolding/scaffolding_features_spec.rb +0 -183
- data/spec/system/selenium_spec.rb +1 -4
- data/spec/system/support/system_test_helper.rb +0 -1
- data/spec/system/watir_spec.rb +1 -4
- data/spec/utilities/headless_config_spec.rb +0 -7
- metadata +3 -102
- data/lib/adopter/adopt_menu.rb +0 -146
- data/lib/adopter/converters/base_converter.rb +0 -84
- data/lib/adopter/converters/identity_converter.rb +0 -53
- data/lib/adopter/migration_plan.rb +0 -74
- data/lib/adopter/migrator.rb +0 -96
- data/lib/adopter/plan_builder.rb +0 -275
- data/lib/adopter/project_analyzer.rb +0 -252
- data/lib/adopter/project_detector.rb +0 -157
- data/lib/generators/cucumber/templates/partials/capybara_env.tt +0 -38
- data/lib/generators/cucumber/templates/partials/capybara_world.tt +0 -6
- data/lib/generators/cucumber/templates/performance_feature.tt +0 -5
- data/lib/generators/cucumber/templates/performance_steps.tt +0 -17
- data/lib/generators/cucumber/templates/visual_feature.tt +0 -5
- data/lib/generators/cucumber/templates/visual_steps.tt +0 -19
- data/lib/generators/infrastructure/gitlab_generator.rb +0 -11
- data/lib/generators/infrastructure/templates/gitlab.tt +0 -46
- data/lib/generators/minitest/minitest_generator.rb +0 -35
- data/lib/generators/minitest/templates/accessibility_test.tt +0 -26
- data/lib/generators/minitest/templates/performance_test.tt +0 -18
- data/lib/generators/minitest/templates/test.tt +0 -64
- data/lib/generators/minitest/templates/visual_test.tt +0 -23
- data/lib/generators/rspec/templates/performance_spec.tt +0 -18
- data/lib/generators/rspec/templates/visual_spec.tt +0 -20
- data/lib/generators/templates/common/ruby_version.tt +0 -1
- data/lib/generators/templates/helpers/capybara_helper.tt +0 -32
- data/lib/generators/templates/helpers/debug_helper.tt +0 -190
- data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +0 -7
- data/lib/generators/templates/helpers/partials/debug_start.tt +0 -7
- data/lib/generators/templates/helpers/performance_helper.tt +0 -57
- data/lib/generators/templates/helpers/visual_helper.tt +0 -58
- data/lib/llm/client.rb +0 -79
- data/lib/llm/config.rb +0 -57
- data/lib/llm/prompts.rb +0 -84
- data/lib/llm/provider.rb +0 -27
- data/lib/llm/providers/anthropic_provider.rb +0 -43
- data/lib/llm/providers/ollama_provider.rb +0 -56
- data/lib/llm/providers/openai_provider.rb +0 -42
- data/lib/llm/response_parser.rb +0 -67
- data/lib/plugin/plugin.rb +0 -111
- data/lib/plugin/plugin_exposer.rb +0 -55
- data/lib/scaffolding/crud_generator.rb +0 -94
- data/lib/scaffolding/dry_run_presenter.rb +0 -16
- data/lib/scaffolding/page_introspector.rb +0 -45
- data/lib/scaffolding/scaffold_menu.rb +0 -103
- data/lib/scaffolding/templates/page_from_url.tt +0 -75
- data/lib/scaffolding/templates/spec_from_page.tt +0 -31
- data/lib/scaffolding/templates/spec_from_url.tt +0 -46
- data/lib/scaffolding/url_analyzer.rb +0 -179
- data/sig/adopter/adopt_menu.rbs +0 -25
- data/sig/adopter/converters/base_converter.rbs +0 -23
- data/sig/adopter/converters/identity_converter.rbs +0 -16
- data/sig/adopter/migration_plan.rbs +0 -34
- data/sig/adopter/migrator.rbs +0 -21
- data/sig/adopter/plan_builder.rbs +0 -38
- data/sig/adopter/project_analyzer.rbs +0 -39
- data/sig/adopter/project_detector.rbs +0 -26
- data/sig/generators/infrastructure/gitlab_generator.rbs +0 -4
- data/sig/generators/minitest/minitest_generator.rbs +0 -8
- data/sig/llm/client.rbs +0 -15
- data/sig/llm/config.rbs +0 -20
- data/sig/llm/prompts.rbs +0 -8
- data/sig/llm/provider.rbs +0 -12
- data/sig/llm/providers/anthropic_provider.rbs +0 -16
- data/sig/llm/providers/ollama_provider.rbs +0 -18
- data/sig/llm/providers/openai_provider.rbs +0 -16
- data/sig/llm/response_parser.rbs +0 -13
- data/sig/plugin/plugin.rbs +0 -24
- data/sig/plugin/plugin_exposer.rbs +0 -20
- data/sig/scaffolding/crud_generator.rbs +0 -16
- data/sig/scaffolding/dry_run_presenter.rbs +0 -4
- data/sig/scaffolding/page_introspector.rbs +0 -14
- data/sig/scaffolding/scaffold_menu.rbs +0 -18
- data/sig/scaffolding/url_analyzer.rbs +0 -28
- data/spec/adopter/adopt_menu_spec.rb +0 -176
- data/spec/adopter/converters/identity_converter_spec.rb +0 -145
- data/spec/adopter/migration_plan_spec.rb +0 -113
- data/spec/adopter/migrator_spec.rb +0 -277
- data/spec/adopter/plan_builder_spec.rb +0 -298
- data/spec/adopter/project_analyzer_spec.rb +0 -337
- data/spec/adopter/project_detector_spec.rb +0 -295
- data/spec/generators/generator_spec.rb +0 -23
- data/spec/integration/content/reporter_content_spec.rb +0 -236
- data/spec/integration/content/skip_flags_content_spec.rb +0 -206
- data/spec/integration/generators/debug_helper_spec.rb +0 -68
- data/spec/integration/generators/gitlab_generator_spec.rb +0 -38
- data/spec/integration/generators/lighthouse_addon_spec.rb +0 -132
- data/spec/integration/generators/minitest_generator_spec.rb +0 -64
- data/spec/integration/generators/reporter_spec.rb +0 -159
- data/spec/integration/generators/skip_flags_spec.rb +0 -134
- data/spec/integration/generators/visual_addon_spec.rb +0 -148
- data/spec/llm/client_spec.rb +0 -79
- data/spec/llm/config_spec.rb +0 -92
- data/spec/llm/prompts_spec.rb +0 -49
- data/spec/llm/response_parser_spec.rb +0 -92
- data/spec/menus/adopter_adopt_menu_spec.rb +0 -97
- data/spec/scaffolding/page_introspector_spec.rb +0 -82
- data/spec/scaffolding/url_analyzer_spec.rb +0 -110
- data/spec/system/adopt_matrix_spec.rb +0 -537
- data/spec/system/adopt_spec.rb +0 -225
- data/spec/system/capybara_spec.rb +0 -42
|
@@ -1,190 +0,0 @@
|
|
|
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,7 +0,0 @@
|
|
|
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 %>
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'chunky_png'
|
|
4
|
-
require 'fileutils'
|
|
5
|
-
|
|
6
|
-
module VisualHelper
|
|
7
|
-
BASELINE_DIR = 'visual_baselines'
|
|
8
|
-
DIFF_DIR = 'visual_diffs'
|
|
9
|
-
|
|
10
|
-
def compare_screenshot(name, screenshot_path, threshold: 0.01)
|
|
11
|
-
FileUtils.mkdir_p(BASELINE_DIR)
|
|
12
|
-
FileUtils.mkdir_p(DIFF_DIR)
|
|
13
|
-
|
|
14
|
-
baseline_path = File.join(BASELINE_DIR, "#{name}.png")
|
|
15
|
-
|
|
16
|
-
unless File.exist?(baseline_path)
|
|
17
|
-
FileUtils.cp(screenshot_path, baseline_path)
|
|
18
|
-
return { status: :baseline_created, path: baseline_path }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
baseline = ChunkyPNG::Image.from_file(baseline_path)
|
|
22
|
-
current = ChunkyPNG::Image.from_file(screenshot_path)
|
|
23
|
-
|
|
24
|
-
diff_pixels = 0
|
|
25
|
-
total_pixels = baseline.width * baseline.height
|
|
26
|
-
|
|
27
|
-
diff_image = ChunkyPNG::Image.new(baseline.width, baseline.height)
|
|
28
|
-
|
|
29
|
-
baseline.height.times do |y|
|
|
30
|
-
baseline.width.times do |x|
|
|
31
|
-
if pixel_match?(baseline[x, y], current[x, y])
|
|
32
|
-
diff_image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 50)
|
|
33
|
-
else
|
|
34
|
-
diff_pixels += 1
|
|
35
|
-
diff_image[x, y] = ChunkyPNG::Color.rgb(255, 0, 0)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
diff_percentage = diff_pixels.to_f / total_pixels
|
|
41
|
-
|
|
42
|
-
if diff_percentage > threshold
|
|
43
|
-
diff_path = File.join(DIFF_DIR, "#{name}_diff.png")
|
|
44
|
-
diff_image.save(diff_path)
|
|
45
|
-
{ status: :mismatch, diff: diff_percentage, diff_path: }
|
|
46
|
-
else
|
|
47
|
-
{ status: :match, diff: diff_percentage }
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def pixel_match?(pixel1, pixel2)
|
|
54
|
-
r1, g1, b1 = ChunkyPNG::Color.r(pixel1), ChunkyPNG::Color.g(pixel1), ChunkyPNG::Color.b(pixel1)
|
|
55
|
-
r2, g2, b2 = ChunkyPNG::Color.r(pixel2), ChunkyPNG::Color.g(pixel2), ChunkyPNG::Color.b(pixel2)
|
|
56
|
-
(r1 - r2).abs <= 5 && (g1 - g2).abs <= 5 && (b1 - b2).abs <= 5
|
|
57
|
-
end
|
|
58
|
-
end
|
data/lib/llm/client.rb
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require_relative 'config'
|
|
5
|
-
|
|
6
|
-
module Llm
|
|
7
|
-
# Facade for LLM completion with retry logic and graceful fallback.
|
|
8
|
-
# Returns nil on any failure — callers should always have a non-AI fallback.
|
|
9
|
-
module Client
|
|
10
|
-
MAX_RETRIES = 3
|
|
11
|
-
BASE_DELAY = 1
|
|
12
|
-
|
|
13
|
-
# Only retry on transient network errors, not configuration or API errors
|
|
14
|
-
RETRYABLE_ERRORS = [
|
|
15
|
-
Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED,
|
|
16
|
-
Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError
|
|
17
|
-
].freeze
|
|
18
|
-
|
|
19
|
-
class << self
|
|
20
|
-
def complete(prompt, system_prompt: nil)
|
|
21
|
-
provider = build_provider
|
|
22
|
-
return nil unless provider
|
|
23
|
-
|
|
24
|
-
attempt = 0
|
|
25
|
-
begin
|
|
26
|
-
attempt += 1
|
|
27
|
-
provider.complete(prompt, system_prompt:)
|
|
28
|
-
rescue *RETRYABLE_ERRORS => e
|
|
29
|
-
if attempt < MAX_RETRIES
|
|
30
|
-
sleep(BASE_DELAY * (2**(attempt - 1)))
|
|
31
|
-
retry
|
|
32
|
-
end
|
|
33
|
-
warn "[Ruby Raider] LLM failed after #{MAX_RETRIES} attempts: #{e.message}"
|
|
34
|
-
nil
|
|
35
|
-
rescue StandardError => e
|
|
36
|
-
warn "[Ruby Raider] LLM error: #{e.message}"
|
|
37
|
-
nil
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def available?
|
|
42
|
-
config = Config.new
|
|
43
|
-
return false unless config.configured?
|
|
44
|
-
|
|
45
|
-
provider = config.build_provider
|
|
46
|
-
provider&.available? || false
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def status
|
|
50
|
-
config = Config.new
|
|
51
|
-
return { configured: false, provider: nil } unless config.configured?
|
|
52
|
-
|
|
53
|
-
provider = config.build_provider
|
|
54
|
-
{
|
|
55
|
-
configured: true,
|
|
56
|
-
provider: config.provider_name,
|
|
57
|
-
model: config.model,
|
|
58
|
-
available: provider&.available? || false
|
|
59
|
-
}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def build_provider
|
|
65
|
-
config = Config.new
|
|
66
|
-
unless config.configured?
|
|
67
|
-
warn '[Ruby Raider] No LLM configured. Use: raider u llm ollama'
|
|
68
|
-
return nil
|
|
69
|
-
end
|
|
70
|
-
provider = config.build_provider
|
|
71
|
-
unless provider&.available?
|
|
72
|
-
warn "[Ruby Raider] LLM provider '#{config.provider_name}' is not available"
|
|
73
|
-
return nil
|
|
74
|
-
end
|
|
75
|
-
provider
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
data/lib/llm/config.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'yaml'
|
|
4
|
-
|
|
5
|
-
module Llm
|
|
6
|
-
# Reads LLM configuration from env vars and config/config.yml
|
|
7
|
-
# Env vars take precedence over config file values.
|
|
8
|
-
class Config
|
|
9
|
-
PROVIDERS = %w[openai anthropic ollama].freeze
|
|
10
|
-
|
|
11
|
-
attr_reader :provider_name, :api_key, :model, :url
|
|
12
|
-
|
|
13
|
-
def initialize
|
|
14
|
-
@provider_name = env('RUBY_RAIDER_LLM_PROVIDER') || config_value('llm_provider')
|
|
15
|
-
@api_key = env('RUBY_RAIDER_LLM_API_KEY') || config_value('llm_api_key')
|
|
16
|
-
@model = env('RUBY_RAIDER_LLM_MODEL') || config_value('llm_model')
|
|
17
|
-
@url = env('RUBY_RAIDER_LLM_URL') || config_value('llm_url')
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def configured?
|
|
21
|
-
return false unless @provider_name && PROVIDERS.include?(@provider_name)
|
|
22
|
-
return true if @provider_name == 'ollama'
|
|
23
|
-
|
|
24
|
-
!@api_key.nil? && !@api_key.empty?
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def build_provider
|
|
28
|
-
return nil unless configured?
|
|
29
|
-
|
|
30
|
-
case @provider_name
|
|
31
|
-
when 'openai'
|
|
32
|
-
require_relative 'providers/openai_provider'
|
|
33
|
-
Providers::OpenaiProvider.new(api_key: @api_key, model: @model)
|
|
34
|
-
when 'anthropic'
|
|
35
|
-
require_relative 'providers/anthropic_provider'
|
|
36
|
-
Providers::AnthropicProvider.new(api_key: @api_key, model: @model)
|
|
37
|
-
when 'ollama'
|
|
38
|
-
require_relative 'providers/ollama_provider'
|
|
39
|
-
Providers::OllamaProvider.new(model: @model, url: @url)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def env(key)
|
|
46
|
-
value = ENV.fetch(key, nil)
|
|
47
|
-
value&.strip&.empty? ? nil : value&.strip
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def config_value(key)
|
|
51
|
-
return nil unless File.exist?('config/config.yml')
|
|
52
|
-
|
|
53
|
-
data = YAML.load_file('config/config.yml')
|
|
54
|
-
data&.dig(key)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
data/lib/llm/prompts.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Llm
|
|
4
|
-
# Centralized prompt templates for LLM-enhanced code generation.
|
|
5
|
-
# All prompts instruct the model to return JSON matching specific schemas.
|
|
6
|
-
module Prompts
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
def analyze_page(html, url)
|
|
10
|
-
<<~PROMPT
|
|
11
|
-
Analyze this HTML page and extract all interactive elements that would be useful for a page object model in UI test automation.
|
|
12
|
-
|
|
13
|
-
URL: #{url}
|
|
14
|
-
|
|
15
|
-
HTML:
|
|
16
|
-
#{html[0..8000]}
|
|
17
|
-
|
|
18
|
-
Return a JSON object with this exact structure:
|
|
19
|
-
{
|
|
20
|
-
"elements": [
|
|
21
|
-
{
|
|
22
|
-
"name": "descriptive_snake_case_name",
|
|
23
|
-
"type": "input|select|textarea|button|submit|link",
|
|
24
|
-
"locator": {"type": "id|name|css|xpath", "value": "the_locator"},
|
|
25
|
-
"purpose": "brief description of what this element does",
|
|
26
|
-
"input_type": "text|email|password|etc (only for inputs)",
|
|
27
|
-
"text": "visible text (only for buttons/links)"
|
|
28
|
-
}
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
Guidelines:
|
|
33
|
-
- Use semantic, descriptive names (e.g., "email_field" not "input_1", "submit_login" not "button_1")
|
|
34
|
-
- Prefer ID locators, then name, then CSS, then XPath
|
|
35
|
-
- Skip hidden inputs and purely decorative elements
|
|
36
|
-
- Include up to 5 important links
|
|
37
|
-
- The "purpose" field should be a brief, clear description
|
|
38
|
-
|
|
39
|
-
Return ONLY the JSON object, no other text.
|
|
40
|
-
PROMPT
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def generate_test_scenarios(class_name, methods, automation, framework)
|
|
44
|
-
methods_desc = methods.map do |m|
|
|
45
|
-
params = m[:params].empty? ? '' : "(#{m[:params].join(', ')})"
|
|
46
|
-
" - #{m[:name]}#{params}"
|
|
47
|
-
end.join("\n")
|
|
48
|
-
|
|
49
|
-
<<~PROMPT
|
|
50
|
-
Generate meaningful test scenarios for this page object class used in UI test automation.
|
|
51
|
-
|
|
52
|
-
Class: #{class_name}
|
|
53
|
-
Automation: #{automation}
|
|
54
|
-
Framework: #{framework}
|
|
55
|
-
Public methods:
|
|
56
|
-
#{methods_desc}
|
|
57
|
-
|
|
58
|
-
Return a JSON object with this exact structure:
|
|
59
|
-
{
|
|
60
|
-
"scenarios": [
|
|
61
|
-
{
|
|
62
|
-
"method": "method_name",
|
|
63
|
-
"description": "human-readable test description",
|
|
64
|
-
"assertion_hint": "what to assert after calling this method"
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
Guidelines:
|
|
70
|
-
- Generate one scenario per method
|
|
71
|
-
- Descriptions should read naturally (e.g., "fills in the login form with valid credentials")
|
|
72
|
-
- Assertion hints should be specific (e.g., "expect page to redirect to dashboard")
|
|
73
|
-
- Consider the class name for context about what page this is
|
|
74
|
-
|
|
75
|
-
Return ONLY the JSON object, no other text.
|
|
76
|
-
PROMPT
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def system_prompt
|
|
80
|
-
'You are a UI test automation expert. You generate clean, idiomatic Ruby code ' \
|
|
81
|
-
'for page object models and test specs. Always return valid JSON when asked for JSON.'
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
data/lib/llm/provider.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Llm
|
|
4
|
-
# Abstract base class for LLM providers
|
|
5
|
-
class Provider
|
|
6
|
-
def complete(prompt, system_prompt: nil)
|
|
7
|
-
raise NotImplementedError, "#{self.class}#complete must be implemented"
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def available?
|
|
11
|
-
raise NotImplementedError, "#{self.class}#available? must be implemented"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def name
|
|
15
|
-
self.class.name.split('::').last.sub('Provider', '').downcase
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def build_messages(prompt, system_prompt)
|
|
21
|
-
messages = []
|
|
22
|
-
messages << { role: 'system', content: system_prompt } if system_prompt
|
|
23
|
-
messages << { role: 'user', content: prompt }
|
|
24
|
-
messages
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../provider'
|
|
4
|
-
|
|
5
|
-
module Llm
|
|
6
|
-
module Providers
|
|
7
|
-
class AnthropicProvider < Provider
|
|
8
|
-
DEFAULT_MODEL = 'claude-sonnet-4-20250514'
|
|
9
|
-
|
|
10
|
-
def initialize(api_key:, model: nil)
|
|
11
|
-
super()
|
|
12
|
-
@api_key = api_key
|
|
13
|
-
@model = model || DEFAULT_MODEL
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def complete(prompt, system_prompt: nil)
|
|
17
|
-
client = build_client
|
|
18
|
-
params = { model: @model, max_tokens: 4096, messages: [{ role: 'user', content: prompt }] }
|
|
19
|
-
params[:system] = system_prompt if system_prompt
|
|
20
|
-
response = client.messages(parameters: params)
|
|
21
|
-
response.dig('content', 0, 'text')
|
|
22
|
-
rescue StandardError => e
|
|
23
|
-
warn "[Ruby Raider] Anthropic error: #{e.message}"
|
|
24
|
-
nil
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def available?
|
|
28
|
-
require 'anthropic'
|
|
29
|
-
!@api_key.nil? && !@api_key.empty?
|
|
30
|
-
rescue LoadError
|
|
31
|
-
warn '[Ruby Raider] Install anthropic gem: gem install anthropic-rb'
|
|
32
|
-
false
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def build_client
|
|
38
|
-
require 'anthropic'
|
|
39
|
-
Anthropic::Client.new(access_token: @api_key)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require_relative '../provider'
|
|
7
|
-
|
|
8
|
-
module Llm
|
|
9
|
-
module Providers
|
|
10
|
-
class OllamaProvider < Provider
|
|
11
|
-
DEFAULT_MODEL = 'llama3.2'
|
|
12
|
-
DEFAULT_URL = 'http://localhost:11434'
|
|
13
|
-
TIMEOUT = 60
|
|
14
|
-
|
|
15
|
-
def initialize(model: nil, url: nil)
|
|
16
|
-
super()
|
|
17
|
-
@model = model || DEFAULT_MODEL
|
|
18
|
-
@base_url = url || DEFAULT_URL
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def complete(prompt, system_prompt: nil)
|
|
22
|
-
uri = URI("#{@base_url}/api/generate")
|
|
23
|
-
body = { model: @model, prompt:, stream: false }
|
|
24
|
-
body[:system] = system_prompt if system_prompt
|
|
25
|
-
|
|
26
|
-
response = post_request(uri, body)
|
|
27
|
-
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
28
|
-
|
|
29
|
-
JSON.parse(response.body)['response']
|
|
30
|
-
rescue StandardError => e
|
|
31
|
-
warn "[Ruby Raider] Ollama error: #{e.message}"
|
|
32
|
-
nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def available?
|
|
36
|
-
uri = URI("#{@base_url}/api/tags")
|
|
37
|
-
response = Net::HTTP.get_response(uri)
|
|
38
|
-
response.is_a?(Net::HTTPSuccess)
|
|
39
|
-
rescue StandardError
|
|
40
|
-
warn '[Ruby Raider] Ollama not reachable. Start it with: ollama serve'
|
|
41
|
-
false
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def post_request(uri, body)
|
|
47
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
48
|
-
http.read_timeout = TIMEOUT
|
|
49
|
-
http.open_timeout = 10
|
|
50
|
-
request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
|
|
51
|
-
request.body = JSON.generate(body)
|
|
52
|
-
http.request(request)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|