capydash 0.2.0 → 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.2.0"
1
+ module CapyDash
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/capydash.rb CHANGED
@@ -1,64 +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
- # Conditionally load RSpec integration if RSpec is present
3
+ # Auto-setup RSpec integration if RSpec is present
17
4
  if defined?(RSpec)
18
- require "capydash/rspec_integration"
19
- CapyDash::RSpecIntegration.setup!
20
- end
21
-
22
- module CapyDash
23
- class << self
24
- attr_accessor :configuration, :current_test, :config
25
-
26
- def configure
27
- self.configuration ||= OpenStruct.new
28
- yield(configuration)
29
- end
30
-
31
- def config
32
- @config ||= Configuration.load_from_file
33
- end
34
-
35
- def config=(new_config)
36
- @config = new_config
37
- end
38
-
39
- # Convenience methods for common operations
40
- def log_info(message, context = {})
41
- Logger.info(message, context)
42
- end
43
-
44
- def log_error(message, context = {})
45
- Logger.error(message, context)
46
- end
47
-
48
- def handle_error(error, context = {})
49
- ErrorHandler.handle_error(error, context)
50
- end
51
-
52
- def save_test_run(data)
53
- Persistence.save_test_run(data)
54
- end
55
-
56
- def load_test_run(run_id)
57
- Persistence.load_test_run(run_id)
58
- end
59
-
60
- def list_test_runs(limit = 50)
61
- Persistence.list_test_runs(limit)
62
- end
63
- end
5
+ require "capydash/rspec"
6
+ CapyDash::RSpec.setup!
64
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.2.0
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,24 +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
132
- - lib/capydash/rspec_integration.rb
65
+ - lib/capydash/rspec.rb
133
66
  - lib/capydash/templates/report.html.erb
134
- - lib/capydash/test_data_aggregator.rb
135
- - lib/capydash/test_data_collector.rb
136
67
  - lib/capydash/version.rb
137
- - lib/generators/capydash/install_generator.rb
138
- - lib/tasks/capydash.rake
139
68
  homepage: https://github.com/damonclark/capydash
140
69
  licenses:
141
70
  - MIT
@@ -157,5 +86,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
86
  requirements: []
158
87
  rubygems_version: 3.6.7
159
88
  specification_version: 4
160
- summary: Real-time Capybara test dashboard
89
+ summary: Minimal static HTML report generator for RSpec system tests
161
90
  test_files: []