capydash 0.2.0 → 0.2.2

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.
@@ -1,285 +0,0 @@
1
- require 'time'
2
- require 'securerandom'
3
- require 'fileutils'
4
- require 'erb'
5
-
6
- module CapyDash
7
- module RSpecIntegration
8
- class << self
9
- def setup!
10
- return unless defined?(RSpec)
11
- return if @configured
12
-
13
- @results = []
14
- @run_id = nil
15
- @configured = true
16
-
17
- RSpec.configure do |config|
18
- config.before(:suite) do
19
- CapyDash::RSpecIntegration.start_test_run
20
- end
21
-
22
- config.after(:each) do |example|
23
- CapyDash::RSpecIntegration.record_example(example)
24
- end
25
-
26
- config.after(:suite) do
27
- CapyDash::RSpecIntegration.finish_test_run
28
- end
29
- end
30
- end
31
-
32
- def start_test_run
33
- @run_id = generate_run_id
34
- @results = []
35
- @started_at = Time.now
36
- end
37
-
38
- def record_example(example)
39
- return unless @run_id
40
-
41
- execution_result = example.execution_result
42
-
43
- # Map RSpec status to our status format
44
- status = case execution_result.status.to_s
45
- when 'passed'
46
- 'passed'
47
- when 'failed'
48
- 'failed'
49
- when 'pending'
50
- 'pending'
51
- else
52
- 'unknown'
53
- end
54
-
55
- # Extract error message if test failed
56
- error_message = nil
57
- if execution_result.status == :failed && execution_result.exception
58
- error_message = format_exception(execution_result.exception)
59
- end
60
-
61
- # Extract class name from example location
62
- # RSpec examples are typically in files like spec/features/user_spec.rb
63
- # We'll use the file path to determine the "class" name
64
- file_path = example.metadata[:file_path] || ''
65
- class_name = extract_class_name_from_path(file_path)
66
-
67
- # Create test data structure matching Minitest format
68
- test_data = {
69
- test_name: "#{class_name}##{example.full_description}",
70
- steps: [
71
- {
72
- step_name: 'test_execution',
73
- detail: example.full_description,
74
- status: status,
75
- error: error_message
76
- }
77
- ]
78
- }
79
-
80
- # Add location information
81
- if example.metadata[:location]
82
- test_data[:location] = example.metadata[:location]
83
- end
84
-
85
- @results << test_data
86
- end
87
-
88
- def finish_test_run
89
- return unless @run_id
90
- return if @results.empty?
91
-
92
- # Calculate summary statistics
93
- total_tests = @results.length
94
- passed_tests = @results.count { |r| r[:steps].any? { |s| s[:status] == 'passed' } }
95
- failed_tests = @results.count { |r| r[:steps].any? { |s| s[:status] == 'failed' } }
96
- pending_tests = @results.count { |r| r[:steps].any? { |s| s[:status] == 'pending' } }
97
-
98
- # Create run data structure matching Minitest format
99
- run_data = {
100
- id: @run_id,
101
- created_at: @started_at.iso8601,
102
- total_tests: total_tests,
103
- passed_tests: passed_tests,
104
- failed_tests: failed_tests,
105
- tests: @results.map { |r| { test_name: r[:test_name], steps: r[:steps] } }
106
- }
107
-
108
- # Save using the existing persistence layer
109
- CapyDash.save_test_run(run_data)
110
-
111
- # Generate report
112
- generate_report(run_data)
113
-
114
- # Clear state
115
- @run_id = nil
116
- @results = []
117
- end
118
-
119
- private
120
-
121
- def generate_run_id
122
- "#{Time.now.to_i}_#{SecureRandom.hex(4)}"
123
- end
124
-
125
- def extract_class_name_from_path(file_path)
126
- return 'UnknownSpec' if file_path.nil? || file_path.empty?
127
-
128
- # Extract filename without extension and path
129
- filename = File.basename(file_path, '.rb')
130
-
131
- # Convert snake_case to PascalCase
132
- # e.g., "user_spec" -> "UserSpec", "features/user_flow_spec" -> "UserFlowSpec"
133
- filename.split('_').map(&:capitalize).join('')
134
- end
135
-
136
- def format_exception(exception)
137
- return nil unless exception
138
-
139
- message = exception.message || 'Unknown error'
140
- backtrace = exception.backtrace || []
141
-
142
- # Format similar to RSpec's output
143
- formatted = "#{exception.class}: #{message}"
144
-
145
- if backtrace.any?
146
- # Include first few lines of backtrace
147
- formatted += "\n" + backtrace.first(5).map { |line| " #{line}" }.join("\n")
148
- end
149
-
150
- formatted
151
- end
152
-
153
- def generate_report(run_data)
154
- # Use the existing ReportGenerator but with our RSpec data
155
- # We need to adapt it to work with our in-memory data structure
156
- report_dir = File.join(Dir.pwd, "capydash_report")
157
- FileUtils.mkdir_p(report_dir)
158
-
159
- assets_dir = File.join(report_dir, "assets")
160
- FileUtils.mkdir_p(assets_dir)
161
-
162
- screenshots_dir = File.join(report_dir, "screenshots")
163
- FileUtils.mkdir_p(screenshots_dir)
164
-
165
- # Generate HTML report using the same template
166
- html_content = generate_html(run_data, run_data[:created_at])
167
- html_path = File.join(report_dir, "index.html")
168
- File.write(html_path, html_content)
169
-
170
- # Generate CSS and JS - use ReportGenerator's private methods via send
171
- # These methods are private but we need them for RSpec reports
172
- css_content = CapyDash::ReportGenerator.send(:generate_css)
173
- css_path = File.join(assets_dir, "dashboard.css")
174
- File.write(css_path, css_content)
175
-
176
- js_content = CapyDash::ReportGenerator.send(:generate_javascript)
177
- js_path = File.join(assets_dir, "dashboard.js")
178
- File.write(js_path, js_content)
179
-
180
- html_path
181
- end
182
-
183
- def generate_html(test_data, created_at)
184
- # Process test data into a structured format (same as ReportGenerator)
185
- processed_tests = process_test_data(test_data)
186
-
187
- # Calculate summary statistics
188
- total_tests = processed_tests.sum { |test| test[:methods].length }
189
- passed_tests = processed_tests.sum { |test| test[:methods].count { |method| method[:status] == 'passed' } }
190
- failed_tests = total_tests - passed_tests
191
-
192
- # Parse created_at if it's a string, otherwise use Time object
193
- created_at_time = if created_at.is_a?(String)
194
- Time.parse(created_at)
195
- else
196
- created_at
197
- end
198
-
199
- # Generate HTML using ERB template
200
- template = File.read(File.join(__dir__, 'templates', 'report.html.erb'))
201
- erb = ERB.new(template)
202
-
203
- erb.result(binding)
204
- end
205
-
206
- def process_test_data(test_data)
207
- return [] unless test_data[:tests]
208
-
209
- # Group tests by class
210
- tests_by_class = {}
211
-
212
- test_data[:tests].each do |test|
213
- test_name = test[:test_name] || 'UnknownTest'
214
-
215
- # Extract class and method names from test name like "UserSpec#should visit the home page"
216
- if test_name.include?('#')
217
- class_name, method_name = test_name.split('#', 2)
218
- else
219
- class_name = extract_class_name(test_name)
220
- method_name = extract_method_name(test_name)
221
- end
222
-
223
- tests_by_class[class_name] ||= {
224
- class_name: class_name,
225
- methods: []
226
- }
227
-
228
- # Process steps
229
- steps = test[:steps] || []
230
- processed_steps = steps.map do |step|
231
- {
232
- name: step[:step_name] || step[:name] || 'unknown_step',
233
- detail: step[:detail] || step[:description] || '',
234
- status: step[:status] || 'unknown',
235
- screenshot: step[:screenshot] ? File.basename(step[:screenshot]) : nil,
236
- error: step[:error] || step[:message]
237
- }
238
- end
239
-
240
- # Filter out "running" steps
241
- processed_steps = processed_steps.reject { |step| step[:status] == 'running' }
242
-
243
- # Determine method status
244
- method_status = if processed_steps.any? { |s| s[:status] == 'failed' }
245
- 'failed'
246
- elsif processed_steps.any? { |s| s[:status] == 'passed' }
247
- 'passed'
248
- elsif processed_steps.any? { |s| s[:status] == 'pending' }
249
- 'pending'
250
- else
251
- 'running'
252
- end
253
-
254
- tests_by_class[class_name][:methods] << {
255
- name: method_name,
256
- status: method_status,
257
- steps: processed_steps
258
- }
259
- end
260
-
261
- tests_by_class.values
262
- end
263
-
264
- def extract_class_name(test_name)
265
- return 'UnknownTest' if test_name.nil? || test_name.empty?
266
-
267
- if test_name.include?('#')
268
- test_name.split('#').first
269
- else
270
- test_name
271
- end
272
- end
273
-
274
- def extract_method_name(test_name)
275
- return 'unknown_method' if test_name.nil? || test_name.empty?
276
-
277
- if test_name.include?('#')
278
- test_name.split('#').last
279
- else
280
- test_name
281
- end
282
- end
283
- end
284
- end
285
- end
@@ -1,221 +0,0 @@
1
- require 'json'
2
- require 'time'
3
- require 'securerandom'
4
- require 'fileutils'
5
-
6
- module CapyDash
7
- class TestDataAggregator
8
- class << self
9
- def start_test_run
10
- # Don't start a new run if one is already in progress
11
- return if test_run_started?
12
-
13
- run_id = generate_run_id
14
- run_data = {
15
- id: run_id,
16
- created_at: Time.now.iso8601,
17
- total_tests: 0,
18
- passed_tests: 0,
19
- failed_tests: 0,
20
- tests: []
21
- }
22
-
23
- # Save initial run data to file
24
- save_run_data(run_data)
25
-
26
- # Set current test context
27
- set_current_test_context(run_id, nil, [])
28
- end
29
-
30
- def test_run_started?
31
- get_current_run_id != nil
32
- end
33
-
34
- def finish_test_run
35
- run_id = get_current_run_id
36
- return unless run_id
37
-
38
- # Load current run data
39
- run_data = load_run_data(run_id)
40
- return unless run_data
41
-
42
- # Save the final test run data
43
- CapyDash.save_test_run(run_data)
44
-
45
- # Clear current state
46
- clear_current_test_context
47
- end
48
-
49
- def handle_event(event)
50
- run_id = get_current_run_id
51
- return unless run_id
52
-
53
- # Load current run data
54
- run_data = load_run_data(run_id)
55
- return unless run_data
56
-
57
- case event[:step_name]
58
- when 'test_start'
59
- start_new_test(event, run_data)
60
- when 'test_finish'
61
- finish_current_test(event, run_data)
62
- when 'test_result'
63
- # This indicates the test is finished
64
- finish_current_test(event, run_data)
65
- else
66
- # This is a test step (visit, click_button, fill_in, etc.)
67
- # If we don't have a current test, start one
68
- current_test = get_current_test
69
- start_new_test(event, run_data) unless current_test
70
- add_test_step(event, run_data)
71
- end
72
- end
73
-
74
- private
75
-
76
- def start_new_test(event, run_data)
77
- # Extract test name from the current test context
78
- test_name = event[:test_name] || CapyDash.current_test || "unknown_test"
79
-
80
- current_test = {
81
- test_name: test_name,
82
- steps: []
83
- }
84
-
85
- # Set current test context
86
- set_current_test_context(run_data[:id], current_test, [])
87
-
88
- # Add the test_start step
89
- add_test_step(event, run_data)
90
- end
91
-
92
- def finish_current_test(event, run_data)
93
- current_test = get_current_test
94
- return unless current_test
95
-
96
- # Add the test_finish step
97
- add_test_step(event, run_data)
98
-
99
- # Get updated test data
100
- current_test = get_current_test
101
- test_steps = get_current_test_steps
102
-
103
- # Determine test status
104
- test_status = determine_test_status(test_steps)
105
-
106
- # Update counters
107
- run_data[:total_tests] += 1
108
- if test_status == 'passed'
109
- run_data[:passed_tests] += 1
110
- elsif test_status == 'failed'
111
- run_data[:failed_tests] += 1
112
- end
113
-
114
- # Add test to current run
115
- run_data[:tests] << current_test
116
-
117
- # Save updated run data
118
- save_run_data(run_data)
119
-
120
- # Clear current test
121
- set_current_test_context(run_data[:id], nil, [])
122
- end
123
-
124
- def add_test_step(event, run_data)
125
- current_test = get_current_test
126
- return unless current_test
127
-
128
- step = {
129
- step_name: event[:step_name],
130
- detail: event[:detail],
131
- status: event[:status],
132
- test_name: event[:test_name] || CapyDash.current_test || current_test[:test_name]
133
- }
134
-
135
- # Add screenshot if present
136
- if event[:screenshot]
137
- step[:screenshot] = event[:screenshot]
138
- end
139
-
140
- # Add error if present
141
- if event[:error]
142
- step[:error] = event[:error]
143
- end
144
-
145
- # Update current test with new step
146
- current_test[:steps] << step
147
- test_steps = get_current_test_steps + [step]
148
-
149
- # Save updated test context
150
- set_current_test_context(run_data[:id], current_test, test_steps)
151
- end
152
-
153
- def determine_test_status(steps)
154
- return 'failed' if steps.any? { |step| step[:status] == 'failed' }
155
- return 'passed' if steps.any? { |step| step[:status] == 'passed' }
156
- 'running'
157
- end
158
-
159
- def generate_run_id
160
- "#{Time.now.to_i}_#{SecureRandom.hex(4)}"
161
- end
162
-
163
- # File-based storage methods for parallel testing support
164
- def run_data_file(run_id)
165
- File.join(Dir.pwd, "tmp", "capydash_data", "run_#{run_id}.json")
166
- end
167
-
168
- def context_file
169
- File.join(Dir.pwd, "tmp", "capydash_data", "current_context.json")
170
- end
171
-
172
- def save_run_data(run_data)
173
- FileUtils.mkdir_p(File.dirname(run_data_file(run_data[:id])))
174
- File.write(run_data_file(run_data[:id]), run_data.to_json)
175
- end
176
-
177
- def load_run_data(run_id)
178
- file_path = run_data_file(run_id)
179
- return nil unless File.exist?(file_path)
180
-
181
- JSON.parse(File.read(file_path), symbolize_names: true)
182
- end
183
-
184
- def set_current_test_context(run_id, current_test, test_steps)
185
- context = {
186
- run_id: run_id,
187
- current_test: current_test,
188
- test_steps: test_steps
189
- }
190
-
191
- FileUtils.mkdir_p(File.dirname(context_file))
192
- File.write(context_file, context.to_json)
193
- end
194
-
195
- def get_current_run_id
196
- return nil unless File.exist?(context_file)
197
-
198
- context = JSON.parse(File.read(context_file), symbolize_names: true)
199
- context[:run_id]
200
- end
201
-
202
- def get_current_test
203
- return nil unless File.exist?(context_file)
204
-
205
- context = JSON.parse(File.read(context_file), symbolize_names: true)
206
- context[:current_test]
207
- end
208
-
209
- def get_current_test_steps
210
- return [] unless File.exist?(context_file)
211
-
212
- context = JSON.parse(File.read(context_file), symbolize_names: true)
213
- context[:test_steps] || []
214
- end
215
-
216
- def clear_current_test_context
217
- File.delete(context_file) if File.exist?(context_file)
218
- end
219
- end
220
- end
221
- end
@@ -1,58 +0,0 @@
1
- require 'capydash/event_emitter'
2
-
3
- module CapyDash
4
- class TestDataCollector
5
- class << self
6
- def start_test_run
7
- @test_run_started = true
8
- @test_count = 0
9
- @passed_count = 0
10
- @failed_count = 0
11
- end
12
-
13
- def finish_test_run
14
- return unless @test_run_started
15
-
16
- @test_run_started = false
17
- end
18
-
19
- def start_test(test_name, test_class, test_method)
20
- return unless @test_run_started
21
-
22
- @test_count += 1
23
-
24
- # Emit test start event
25
- CapyDash::EventEmitter.broadcast(
26
- step_name: "test_start",
27
- detail: "Starting test: #{test_name}",
28
- test_name: test_name,
29
- status: "running"
30
- )
31
- end
32
-
33
- def finish_test(test_name, status, error_message = nil)
34
- return unless @test_run_started
35
-
36
- if status == "passed"
37
- @passed_count += 1
38
- elsif status == "failed"
39
- @failed_count += 1
40
- end
41
-
42
- # Emit test finish event
43
- event_data = {
44
- step_name: "test_finish",
45
- detail: "Test #{status}: #{test_name}",
46
- test_name: test_name,
47
- status: status
48
- }
49
-
50
- if error_message
51
- event_data[:error] = error_message
52
- end
53
-
54
- CapyDash::EventEmitter.broadcast(event_data)
55
- end
56
- end
57
- end
58
- end
@@ -1,124 +0,0 @@
1
- require 'rails/generators'
2
-
3
- module Capydash
4
- module Generators
5
- class InstallGenerator < Rails::Generators::Base
6
- source_root File.expand_path('templates', __dir__)
7
-
8
- desc "Installs CapyDash with all necessary configuration files"
9
-
10
- def create_initializer
11
- create_file "config/initializers/capydash.rb", <<~RUBY
12
- require 'capydash'
13
-
14
- # Configure CapyDash
15
- CapyDash.configure do |config|
16
- config.port = 4000
17
- config.screenshot_path = "tmp/capydash_screenshots"
18
- end
19
-
20
- # Subscribe to events for test data collection
21
- CapyDash::EventEmitter.subscribe do |event|
22
- # Collect test data for report generation
23
- CapyDash::TestDataAggregator.handle_event(event)
24
- end
25
- RUBY
26
- end
27
-
28
- def create_rake_tasks
29
- create_file "lib/tasks/capydash.rake", <<~RUBY
30
- namespace :capydash do
31
- desc "Generate static HTML test report"
32
- task :report => :environment do
33
- CapyDash::ReportGenerator.generate_report
34
- end
35
-
36
- desc "Start local server to view static HTML report"
37
- task :server => :environment do
38
- CapyDash::DashboardServer.start
39
- end
40
- end
41
- RUBY
42
- end
43
-
44
- def update_test_helper
45
- test_helper_path = "test/test_helper.rb"
46
-
47
- if File.exist?(test_helper_path)
48
- # Read existing test helper
49
- content = File.read(test_helper_path)
50
-
51
- # Check if CapyDash is already configured
52
- unless content.include?("require 'capydash'")
53
- # Add CapyDash configuration
54
- capydash_config = <<~RUBY
55
-
56
- # CapyDash configuration
57
- require 'capydash'
58
-
59
- # Start test run data collection
60
- CapyDash::TestDataCollector.start_test_run
61
-
62
- # Hook into test execution to set current test name and manage test runs
63
- module CapyDash
64
- module TestHooks
65
- def run(&block)
66
- # Set the current test name for CapyDash
67
- CapyDash.current_test = self.name
68
-
69
- # Start test run data collection if not already started
70
- CapyDash::TestDataAggregator.start_test_run unless CapyDash::TestDataAggregator.instance_variable_get(:@current_run)
71
-
72
- super
73
- end
74
- end
75
- end
76
-
77
- # Apply the hook to the test case
78
- class ActiveSupport::TestCase
79
- prepend CapyDash::TestHooks
80
- end
81
-
82
- # Hook to finish test run when all tests are done
83
- Minitest.after_run do
84
- CapyDash::TestDataCollector.finish_test_run
85
- CapyDash::TestDataAggregator.finish_test_run
86
- end
87
- RUBY
88
-
89
- # Insert after the last require statement
90
- if content.match(/require.*\n/)
91
- content = content.gsub(/(require.*\n)/, "\\1#{capydash_config}")
92
- else
93
- content = capydash_config + content
94
- end
95
-
96
- File.write(test_helper_path, content)
97
- say "Updated test/test_helper.rb with CapyDash configuration"
98
- else
99
- say "CapyDash already configured in test/test_helper.rb", :yellow
100
- end
101
- else
102
- say "test/test_helper.rb not found. Please add CapyDash configuration manually.", :red
103
- end
104
- end
105
-
106
-
107
- def show_instructions
108
- say "\n" + "="*60, :green
109
- say "CapyDash has been successfully installed!", :green
110
- say "="*60, :green
111
- say "\nNext steps:", :yellow
112
- say "1. Run your tests: bundle exec rails test"
113
- say "2. Generate report: bundle exec rake capydash:report"
114
- say "3. View report: open capydash_report/index.html"
115
- say "\nImportant:", :yellow
116
- say "- CapyDash only captures system tests that use Capybara methods (visit, click, fill_in, etc.)"
117
- say "- Unit tests and integration tests without Capybara won't appear in the report"
118
- say "- Works with parallel testing - no configuration needed"
119
- say "\nFor more information, see the README.md file."
120
- say "="*60, :green
121
- end
122
- end
123
- end
124
- end