capydash 0.1.7 → 0.2.1

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.
@@ -0,0 +1,415 @@
1
+ require 'time'
2
+ require 'fileutils'
3
+ require 'erb'
4
+
5
+ module CapyDash
6
+ module RSpec
7
+ class << self
8
+ def setup!
9
+ return unless defined?(::RSpec)
10
+ return if @configured
11
+
12
+ @configured = true
13
+ @results = []
14
+ @started_at = nil
15
+
16
+ ::RSpec.configure do |config|
17
+ config.before(:suite) do
18
+ CapyDash::RSpec.start_run
19
+ end
20
+
21
+ config.after(:each) do |example|
22
+ CapyDash::RSpec.record_example(example)
23
+ end
24
+
25
+ config.after(:suite) do
26
+ CapyDash::RSpec.generate_report
27
+ end
28
+ end
29
+ end
30
+
31
+ def start_run
32
+ @results = []
33
+ @started_at = Time.now
34
+ end
35
+
36
+ def record_example(example)
37
+ return unless @started_at
38
+
39
+ execution_result = example.execution_result
40
+ status = execution_result.status.to_s
41
+
42
+ error_message = nil
43
+ if execution_result.status == :failed && execution_result.exception
44
+ error_message = format_exception(execution_result.exception)
45
+ end
46
+
47
+ file_path = example.metadata[:file_path] || ''
48
+ class_name = extract_class_name(file_path)
49
+
50
+ @results << {
51
+ class_name: class_name,
52
+ method_name: example.full_description,
53
+ status: status,
54
+ error: error_message,
55
+ location: example.metadata[:location]
56
+ }
57
+ end
58
+
59
+ def generate_report
60
+ return unless @started_at
61
+ return if @results.empty?
62
+
63
+ report_dir = File.join(Dir.pwd, "capydash_report")
64
+ FileUtils.mkdir_p(report_dir)
65
+
66
+ assets_dir = File.join(report_dir, "assets")
67
+ FileUtils.mkdir_p(assets_dir)
68
+
69
+ # Group results by class
70
+ tests_by_class = @results.group_by { |r| r[:class_name] }
71
+
72
+ # Calculate statistics
73
+ total_tests = @results.length
74
+ passed_tests = @results.count { |r| r[:status] == 'passed' }
75
+ failed_tests = @results.count { |r| r[:status] == 'failed' }
76
+
77
+ # Process for template
78
+ processed_tests = tests_by_class.map do |class_name, examples|
79
+ {
80
+ class_name: class_name,
81
+ methods: examples.map do |ex|
82
+ {
83
+ name: ex[:method_name],
84
+ status: ex[:status],
85
+ steps: [{
86
+ name: 'test_execution',
87
+ detail: ex[:method_name],
88
+ status: ex[:status],
89
+ error: ex[:error]
90
+ }]
91
+ }
92
+ end
93
+ }
94
+ end
95
+
96
+ # Generate HTML
97
+ html_content = generate_html(processed_tests, @started_at, total_tests, passed_tests, failed_tests)
98
+ File.write(File.join(report_dir, "index.html"), html_content)
99
+
100
+ # Generate CSS
101
+ css_content = generate_css
102
+ File.write(File.join(assets_dir, "dashboard.css"), css_content)
103
+
104
+ # Generate JS
105
+ js_content = generate_javascript
106
+ File.write(File.join(assets_dir, "dashboard.js"), js_content)
107
+
108
+ report_dir
109
+ end
110
+
111
+ private
112
+
113
+ def extract_class_name(file_path)
114
+ return 'UnknownSpec' if file_path.nil? || file_path.empty?
115
+
116
+ filename = File.basename(file_path, '.rb')
117
+ filename.split('_').map(&:capitalize).join('')
118
+ end
119
+
120
+ def format_exception(exception)
121
+ return nil unless exception
122
+
123
+ message = exception.message || 'Unknown error'
124
+ backtrace = exception.backtrace || []
125
+
126
+ formatted = "#{exception.class}: #{message}"
127
+ if backtrace.any?
128
+ formatted += "\n" + backtrace.first(5).map { |line| " #{line}" }.join("\n")
129
+ end
130
+
131
+ formatted
132
+ end
133
+
134
+ def generate_html(processed_tests, created_at, total_tests, passed_tests, failed_tests)
135
+ # Create safe IDs for method names (escape special chars for HTML/JS)
136
+ processed_tests.each do |test_class|
137
+ test_class[:methods].each do |method|
138
+ method[:safe_id] = method[:name].gsub(/['"]/, '').gsub(/[^a-zA-Z0-9]/, '_')
139
+ end
140
+ end
141
+
142
+ template_path = File.join(__dir__, 'templates', 'report.html.erb')
143
+ template = File.read(template_path)
144
+ erb = ERB.new(template)
145
+
146
+ erb.result(binding)
147
+ end
148
+
149
+ def generate_css
150
+ <<~CSS
151
+ * {
152
+ margin: 0;
153
+ padding: 0;
154
+ box-sizing: border-box;
155
+ }
156
+
157
+ body {
158
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
159
+ line-height: 1.6;
160
+ color: #333;
161
+ background-color: #f8f9fa;
162
+ }
163
+
164
+ .container {
165
+ max-width: 1400px;
166
+ margin: 0 auto;
167
+ padding: 1rem;
168
+ }
169
+
170
+ .header {
171
+ background: white;
172
+ padding: 1.5rem;
173
+ border-radius: 8px;
174
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
175
+ margin-bottom: 1.5rem;
176
+ }
177
+
178
+ .header h1 {
179
+ font-size: 2rem;
180
+ margin-bottom: 0.5rem;
181
+ color: #2c3e50;
182
+ }
183
+
184
+ .header .subtitle {
185
+ color: #666;
186
+ font-size: 0.9rem;
187
+ }
188
+
189
+ .summary {
190
+ display: grid;
191
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
192
+ gap: 1rem;
193
+ margin-bottom: 1.5rem;
194
+ }
195
+
196
+ .summary-card {
197
+ background: white;
198
+ padding: 1.5rem;
199
+ border-radius: 8px;
200
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
201
+ text-align: center;
202
+ }
203
+
204
+ .summary-card .number {
205
+ font-size: 2.5rem;
206
+ font-weight: bold;
207
+ margin-bottom: 0.5rem;
208
+ }
209
+
210
+ .summary-card.total .number {
211
+ color: #3498db;
212
+ }
213
+
214
+ .summary-card.passed .number {
215
+ color: #27ae60;
216
+ }
217
+
218
+ .summary-card.failed .number {
219
+ color: #e74c3c;
220
+ }
221
+
222
+ .summary-card .label {
223
+ color: #666;
224
+ font-size: 0.9rem;
225
+ text-transform: uppercase;
226
+ letter-spacing: 0.5px;
227
+ }
228
+
229
+ .test-results {
230
+ background: white;
231
+ border-radius: 8px;
232
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
233
+ overflow: hidden;
234
+ }
235
+
236
+ .test-class {
237
+ border-bottom: 1px solid #eee;
238
+ }
239
+
240
+ .test-class:last-child {
241
+ border-bottom: none;
242
+ }
243
+
244
+ .test-class h2 {
245
+ background: #f8f9fa;
246
+ padding: 1rem 1.5rem;
247
+ margin: 0;
248
+ font-size: 1.25rem;
249
+ color: #2c3e50;
250
+ border-bottom: 1px solid #eee;
251
+ }
252
+
253
+ .test-method {
254
+ padding: 1.5rem;
255
+ border-bottom: 1px solid #f0f0f0;
256
+ }
257
+
258
+ .test-method:last-child {
259
+ border-bottom: none;
260
+ }
261
+
262
+ .test-method-header {
263
+ display: flex;
264
+ align-items: center;
265
+ margin-bottom: 1rem;
266
+ cursor: pointer;
267
+ gap: 0.75rem;
268
+ }
269
+
270
+ .test-method h3 {
271
+ margin: 0;
272
+ font-size: 1.1rem;
273
+ color: #34495e;
274
+ flex: 1;
275
+ }
276
+
277
+ .expand-toggle {
278
+ background: none;
279
+ border: none;
280
+ cursor: pointer;
281
+ padding: 0.5rem;
282
+ border-radius: 4px;
283
+ transition: background-color 0.2s;
284
+ }
285
+
286
+ .expand-toggle:hover {
287
+ background-color: #f0f0f0;
288
+ }
289
+
290
+ .expand-icon {
291
+ font-size: 0.8rem;
292
+ color: #666;
293
+ }
294
+
295
+ .steps {
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: 1rem;
299
+ transition: max-height 0.3s ease-out;
300
+ overflow: hidden;
301
+ }
302
+
303
+ .steps.collapsed {
304
+ max-height: 0;
305
+ margin: 0;
306
+ }
307
+
308
+ .step {
309
+ border: 1px solid #ddd;
310
+ border-radius: 6px;
311
+ padding: 1rem;
312
+ background: #fafafa;
313
+ }
314
+
315
+ .step.passed {
316
+ border-color: #27ae60;
317
+ background: #f8fff8;
318
+ }
319
+
320
+ .step.failed {
321
+ border-color: #e74c3c;
322
+ background: #fff8f8;
323
+ }
324
+
325
+ .step-header {
326
+ display: flex;
327
+ justify-content: space-between;
328
+ align-items: center;
329
+ margin-bottom: 0.5rem;
330
+ }
331
+
332
+ .step-name {
333
+ font-weight: 600;
334
+ color: #2c3e50;
335
+ }
336
+
337
+ .step-status {
338
+ padding: 0.25rem 0.5rem;
339
+ border-radius: 4px;
340
+ font-size: 0.8rem;
341
+ font-weight: 600;
342
+ text-transform: uppercase;
343
+ }
344
+
345
+ .step.passed .step-status {
346
+ background: #27ae60;
347
+ color: white;
348
+ }
349
+
350
+ .step.failed .step-status {
351
+ background: #e74c3c;
352
+ color: white;
353
+ }
354
+
355
+ .step-detail {
356
+ color: #666;
357
+ font-size: 0.9rem;
358
+ margin-bottom: 0.5rem;
359
+ }
360
+
361
+ .error-log {
362
+ margin-top: 1rem;
363
+ padding: 1rem;
364
+ background: #fff5f5;
365
+ border: 1px solid #fed7d7;
366
+ border-radius: 6px;
367
+ }
368
+
369
+ .error-log h4 {
370
+ color: #e53e3e;
371
+ margin: 0 0 0.5rem 0;
372
+ font-size: 0.9rem;
373
+ }
374
+
375
+ .error-log pre {
376
+ background: #2d3748;
377
+ color: #e2e8f0;
378
+ padding: 1rem;
379
+ border-radius: 4px;
380
+ overflow-x: auto;
381
+ font-size: 0.8rem;
382
+ line-height: 1.4;
383
+ margin: 0;
384
+ white-space: pre-wrap;
385
+ word-wrap: break-word;
386
+ }
387
+ CSS
388
+ end
389
+
390
+ def generate_javascript
391
+ <<~JS
392
+ function toggleTestMethod(safeId) {
393
+ const stepsContainer = document.getElementById('steps-' + safeId);
394
+ const button = document.querySelector('[onclick*="' + safeId + '"]');
395
+
396
+ if (stepsContainer && button) {
397
+ const icon = button.querySelector('.expand-icon');
398
+ if (icon) {
399
+ const isCollapsed = stepsContainer.classList.contains('collapsed');
400
+
401
+ if (isCollapsed) {
402
+ stepsContainer.classList.remove('collapsed');
403
+ icon.textContent = '▼';
404
+ } else {
405
+ stepsContainer.classList.add('collapsed');
406
+ icon.textContent = '▶';
407
+ }
408
+ }
409
+ }
410
+ }
411
+ JS
412
+ end
413
+ end
414
+ end
415
+ end
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>CapyDash - Test Dashboard Report</title>
6
+ <title>CapyDash - Test Report</title>
7
7
  <link rel="stylesheet" href="assets/dashboard.css">
8
8
  </head>
9
9
  <body>
@@ -11,14 +11,6 @@
11
11
  <div class="header">
12
12
  <h1>CapyDash Test Report</h1>
13
13
  <div class="subtitle">Generated on <%= created_at.strftime('%B %d, %Y at %I:%M %p') %></div>
14
-
15
- <div class="search-container">
16
- <div class="search-input-wrapper">
17
- <input type="text" id="searchInput" placeholder="Search tests, steps, or errors..." class="search-input" autocomplete="off">
18
- <div id="typeaheadDropdown" class="typeahead-dropdown"></div>
19
- </div>
20
- <div id="searchStats" class="search-stats"></div>
21
- </div>
22
14
  </div>
23
15
 
24
16
  <div class="summary">
@@ -44,23 +36,17 @@
44
36
  <% test_class[:methods].each do |method| %>
45
37
  <div class="test-method">
46
38
  <div class="test-method-header">
47
- <button class="expand-toggle" onclick="toggleTestMethod('<%= method[:name] %>')">
39
+ <button class="expand-toggle" onclick="toggleTestMethod('<%= method[:safe_id] %>')">
48
40
  <span class="expand-icon">▶</span>
49
41
  </button>
50
42
  <h3><%= method[:name] %></h3>
51
43
  </div>
52
- <div class="steps collapsed" id="steps-<%= method[:name] %>">
53
- <% method[:steps].each_with_index do |step, step_index| %>
44
+ <div class="steps collapsed" id="steps-<%= method[:safe_id] %>">
45
+ <% method[:steps].each do |step| %>
54
46
  <div class="step <%= step[:status] %>">
55
47
  <div class="step-header">
56
48
  <span class="step-name"><%= step[:name] %></span>
57
49
  <span class="step-status"><%= step[:status] %></span>
58
-
59
- <% if step[:screenshot] %>
60
- <button class="screenshot-toggle" onclick="toggleScreenshot('<%= method[:name] %>-<%= step_index %>')">
61
- 📸 Screenshot
62
- </button>
63
- <% end %>
64
50
  </div>
65
51
  <div class="step-detail"><%= step[:detail] %></div>
66
52
 
@@ -70,14 +56,6 @@
70
56
  <pre><%= step[:error] %></pre>
71
57
  </div>
72
58
  <% end %>
73
-
74
- <% if step[:screenshot] %>
75
- <div class="screenshot-container" id="screenshot-<%= method[:name] %>-<%= step_index %>" style="display: none;">
76
- <div class="screenshot">
77
- <img src="screenshots/<%= step[:screenshot] %>" alt="Screenshot">
78
- </div>
79
- </div>
80
- <% end %>
81
59
  </div>
82
60
  <% end %>
83
61
  </div>
@@ -1,3 +1,3 @@
1
- module Capydash
2
- VERSION = "0.1.7"
1
+ module CapyDash
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/capydash.rb CHANGED
@@ -1,58 +1,7 @@
1
- require 'ostruct'
2
1
  require "capydash/version"
3
- require "capydash/engine"
4
- require "capydash/instrumentation"
5
- require "capydash/event_emitter"
6
- require "capydash/dashboard_server"
7
- require "capydash/configuration"
8
- require "capydash/logger"
9
- require "capydash/error_handler"
10
- require "capydash/persistence"
11
- require "capydash/auth"
12
- require "capydash/test_data_collector"
13
- require "capydash/test_data_aggregator"
14
- require "capydash/report_generator"
15
2
 
16
- module CapyDash
17
- class << self
18
- attr_accessor :configuration, :current_test, :config
19
-
20
- def configure
21
- self.configuration ||= OpenStruct.new
22
- yield(configuration)
23
- end
24
-
25
- def config
26
- @config ||= Configuration.load_from_file
27
- end
28
-
29
- def config=(new_config)
30
- @config = new_config
31
- end
32
-
33
- # Convenience methods for common operations
34
- def log_info(message, context = {})
35
- Logger.info(message, context)
36
- end
37
-
38
- def log_error(message, context = {})
39
- Logger.error(message, context)
40
- end
41
-
42
- def handle_error(error, context = {})
43
- ErrorHandler.handle_error(error, context)
44
- end
45
-
46
- def save_test_run(data)
47
- Persistence.save_test_run(data)
48
- end
49
-
50
- def load_test_run(run_id)
51
- Persistence.load_test_run(run_id)
52
- end
53
-
54
- def list_test_runs(limit = 50)
55
- Persistence.list_test_runs(limit)
56
- end
57
- end
3
+ # Auto-setup RSpec integration if RSpec is present
4
+ if defined?(RSpec)
5
+ require "capydash/rspec"
6
+ CapyDash::RSpec.setup!
58
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capydash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damon Clark
@@ -10,21 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: railties
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '5.0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '5.0'
26
- - !ruby/object:Gem::Dependency
27
- name: capybara
13
+ name: rspec
28
14
  requirement: !ruby/object:Gem::Requirement
29
15
  requirements:
30
16
  - - ">="
@@ -38,77 +24,35 @@ dependencies:
38
24
  - !ruby/object:Gem::Version
39
25
  version: '3.0'
40
26
  - !ruby/object:Gem::Dependency
41
- name: faye-websocket
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: eventmachine
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- - !ruby/object:Gem::Dependency
69
- name: em-websocket
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
- - !ruby/object:Gem::Dependency
83
- name: rspec
27
+ name: rspec-rails
84
28
  requirement: !ruby/object:Gem::Requirement
85
29
  requirements:
86
30
  - - "~>"
87
31
  - !ruby/object:Gem::Version
88
- version: '3.0'
32
+ version: '6.0'
89
33
  type: :development
90
34
  prerelease: false
91
35
  version_requirements: !ruby/object:Gem::Requirement
92
36
  requirements:
93
37
  - - "~>"
94
38
  - !ruby/object:Gem::Version
95
- version: '3.0'
39
+ version: '6.0'
96
40
  - !ruby/object:Gem::Dependency
97
41
  name: rails
98
42
  requirement: !ruby/object:Gem::Requirement
99
43
  requirements:
100
- - - "~>"
44
+ - - ">="
101
45
  - !ruby/object:Gem::Version
102
- version: '8.0'
46
+ version: '6.0'
103
47
  type: :development
104
48
  prerelease: false
105
49
  version_requirements: !ruby/object:Gem::Requirement
106
50
  requirements:
107
- - - "~>"
51
+ - - ">="
108
52
  - !ruby/object:Gem::Version
109
- version: '8.0'
110
- description: CapyDash instruments Capybara tests and streams test steps, screenshots,
111
- and DOM snapshots to a live dashboard.
53
+ version: '6.0'
54
+ description: CapyDash automatically generates clean, readable HTML test reports after
55
+ your RSpec suite finishes. Zero configuration required.
112
56
  email:
113
57
  - dclark312@gmail.com
114
58
  executables: []
@@ -118,23 +62,9 @@ files:
118
62
  - README.md
119
63
  - capydash.gemspec
120
64
  - lib/capydash.rb
121
- - lib/capydash/auth.rb
122
- - lib/capydash/configuration.rb
123
- - lib/capydash/dashboard_server.rb
124
- - lib/capydash/engine.rb
125
- - lib/capydash/error_handler.rb
126
- - lib/capydash/event_emitter.rb
127
- - lib/capydash/forwarder.rb
128
- - lib/capydash/instrumentation.rb
129
- - lib/capydash/logger.rb
130
- - lib/capydash/persistence.rb
131
- - lib/capydash/report_generator.rb
65
+ - lib/capydash/rspec.rb
132
66
  - lib/capydash/templates/report.html.erb
133
- - lib/capydash/test_data_aggregator.rb
134
- - lib/capydash/test_data_collector.rb
135
67
  - lib/capydash/version.rb
136
- - lib/generators/capydash/install_generator.rb
137
- - lib/tasks/capydash.rake
138
68
  homepage: https://github.com/damonclark/capydash
139
69
  licenses:
140
70
  - MIT
@@ -156,5 +86,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
86
  requirements: []
157
87
  rubygems_version: 3.6.7
158
88
  specification_version: 4
159
- summary: Real-time Capybara test dashboard
89
+ summary: Minimal static HTML report generator for RSpec system tests
160
90
  test_files: []