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.
@@ -0,0 +1,567 @@
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 rspec_available?
10
+ return if @configured
11
+
12
+ begin
13
+ @configured = true
14
+ @results = []
15
+ @started_at = nil
16
+
17
+ ::RSpec.configure do |config|
18
+ config.before(:suite) do
19
+ CapyDash::RSpec.start_run
20
+ end
21
+
22
+ config.after(:each) do |example|
23
+ CapyDash::RSpec.record_example(example)
24
+ end
25
+
26
+ config.after(:suite) do
27
+ CapyDash::RSpec.generate_report
28
+ end
29
+ end
30
+ rescue => e
31
+ # If RSpec isn't ready, silently fail - it will be set up later
32
+ @configured = false
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def rspec_available?
39
+ return false unless defined?(::RSpec)
40
+ return false unless ::RSpec.respond_to?(:configure)
41
+ true
42
+ rescue
43
+ false
44
+ end
45
+
46
+ def start_run
47
+ @results = []
48
+ @started_at = Time.now
49
+ end
50
+
51
+ def record_example(example)
52
+ return unless @started_at
53
+
54
+ execution_result = example.execution_result
55
+ status = execution_result.status.to_s
56
+
57
+ error_message = nil
58
+ if execution_result.status == :failed && execution_result.exception
59
+ error_message = format_exception(execution_result.exception)
60
+ end
61
+
62
+ file_path = example.metadata[:file_path] || ''
63
+ class_name = extract_class_name(file_path)
64
+
65
+ @results << {
66
+ class_name: class_name,
67
+ method_name: example.full_description,
68
+ status: status,
69
+ error: error_message,
70
+ location: example.metadata[:location]
71
+ }
72
+ end
73
+
74
+ def generate_report
75
+ return unless @started_at
76
+ return if @results.empty?
77
+
78
+ report_dir = File.join(Dir.pwd, "capydash_report")
79
+ FileUtils.mkdir_p(report_dir)
80
+
81
+ assets_dir = File.join(report_dir, "assets")
82
+ FileUtils.mkdir_p(assets_dir)
83
+
84
+ # Group results by class
85
+ tests_by_class = @results.group_by { |r| r[:class_name] }
86
+
87
+ # Calculate statistics
88
+ total_tests = @results.length
89
+ passed_tests = @results.count { |r| r[:status] == 'passed' }
90
+ failed_tests = @results.count { |r| r[:status] == 'failed' }
91
+
92
+ # Process for template
93
+ processed_tests = tests_by_class.map do |class_name, examples|
94
+ {
95
+ class_name: class_name,
96
+ methods: examples.map do |ex|
97
+ {
98
+ name: ex[:method_name],
99
+ status: ex[:status],
100
+ steps: [{
101
+ name: 'test_execution',
102
+ detail: ex[:method_name],
103
+ status: ex[:status],
104
+ error: ex[:error]
105
+ }]
106
+ }
107
+ end
108
+ }
109
+ end
110
+
111
+ # Generate HTML
112
+ html_content = generate_html(processed_tests, @started_at, total_tests, passed_tests, failed_tests)
113
+ File.write(File.join(report_dir, "index.html"), html_content)
114
+
115
+ # Generate CSS
116
+ css_content = generate_css
117
+ File.write(File.join(assets_dir, "dashboard.css"), css_content)
118
+
119
+ # Generate JS
120
+ js_content = generate_javascript
121
+ File.write(File.join(assets_dir, "dashboard.js"), js_content)
122
+
123
+ report_dir
124
+ end
125
+
126
+ private
127
+
128
+ def extract_class_name(file_path)
129
+ return 'UnknownSpec' if file_path.nil? || file_path.empty?
130
+
131
+ filename = File.basename(file_path, '.rb')
132
+ filename.split('_').map(&:capitalize).join('')
133
+ end
134
+
135
+ def format_exception(exception)
136
+ return nil unless exception
137
+
138
+ message = exception.message || 'Unknown error'
139
+ backtrace = exception.backtrace || []
140
+
141
+ formatted = "#{exception.class}: #{message}"
142
+ if backtrace.any?
143
+ formatted += "\n" + backtrace.first(5).map { |line| " #{line}" }.join("\n")
144
+ end
145
+
146
+ formatted
147
+ end
148
+
149
+ def generate_html(processed_tests, created_at, total_tests, passed_tests, failed_tests)
150
+ # Create safe IDs for method names (escape special chars for HTML/JS)
151
+ processed_tests.each do |test_class|
152
+ test_class[:methods].each do |method|
153
+ method[:safe_id] = method[:name].gsub(/['"]/, '').gsub(/[^a-zA-Z0-9]/, '_')
154
+ end
155
+ end
156
+
157
+ template_path = File.join(__dir__, 'templates', 'report.html.erb')
158
+ template = File.read(template_path)
159
+ erb = ERB.new(template)
160
+
161
+ erb.result(binding)
162
+ end
163
+
164
+ def generate_css
165
+ <<~CSS
166
+ * {
167
+ margin: 0;
168
+ padding: 0;
169
+ box-sizing: border-box;
170
+ }
171
+
172
+ body {
173
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
174
+ line-height: 1.6;
175
+ color: #333;
176
+ background-color: #f8f9fa;
177
+ }
178
+
179
+ .container {
180
+ max-width: 1400px;
181
+ margin: 0 auto;
182
+ padding: 1rem;
183
+ }
184
+
185
+ .header {
186
+ background: white;
187
+ padding: 1.5rem;
188
+ border-radius: 8px;
189
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
190
+ margin-bottom: 1.5rem;
191
+ }
192
+
193
+ .header h1 {
194
+ font-size: 2rem;
195
+ margin-bottom: 0.5rem;
196
+ color: #2c3e50;
197
+ }
198
+
199
+ .header .subtitle {
200
+ color: #666;
201
+ font-size: 0.9rem;
202
+ margin-bottom: 1rem;
203
+ }
204
+
205
+ .search-container {
206
+ margin-top: 1rem;
207
+ }
208
+
209
+ .search-input {
210
+ width: 100%;
211
+ padding: 0.75rem 1rem;
212
+ border: 2px solid #ddd;
213
+ border-radius: 6px;
214
+ font-size: 1rem;
215
+ transition: border-color 0.2s;
216
+ }
217
+
218
+ .search-input:focus {
219
+ outline: none;
220
+ border-color: #3498db;
221
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
222
+ }
223
+
224
+ .test-method.hidden {
225
+ display: none;
226
+ }
227
+
228
+ .test-class.hidden {
229
+ display: none;
230
+ }
231
+
232
+ .method-status {
233
+ padding: 0.25rem 0.75rem;
234
+ border-radius: 4px;
235
+ font-size: 0.75rem;
236
+ font-weight: 600;
237
+ text-transform: uppercase;
238
+ letter-spacing: 0.5px;
239
+ }
240
+
241
+ .method-status-passed {
242
+ background: #27ae60;
243
+ color: white;
244
+ }
245
+
246
+ .method-status-failed {
247
+ background: #e74c3c;
248
+ color: white;
249
+ }
250
+
251
+ .method-status-pending {
252
+ background: #f39c12;
253
+ color: white;
254
+ }
255
+
256
+ .summary {
257
+ display: grid;
258
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
259
+ gap: 1rem;
260
+ margin-bottom: 1.5rem;
261
+ }
262
+
263
+ .summary-card {
264
+ background: white;
265
+ padding: 1.5rem;
266
+ border-radius: 8px;
267
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
268
+ text-align: center;
269
+ }
270
+
271
+ .summary-card .number {
272
+ font-size: 2.5rem;
273
+ font-weight: bold;
274
+ margin-bottom: 0.5rem;
275
+ }
276
+
277
+ .summary-card.total .number {
278
+ color: #3498db;
279
+ }
280
+
281
+ .summary-card.passed .number {
282
+ color: #27ae60;
283
+ }
284
+
285
+ .summary-card.failed .number {
286
+ color: #e74c3c;
287
+ }
288
+
289
+ .summary-card .label {
290
+ color: #666;
291
+ font-size: 0.9rem;
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.5px;
294
+ }
295
+
296
+ .test-results {
297
+ background: white;
298
+ border-radius: 8px;
299
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
300
+ overflow: hidden;
301
+ }
302
+
303
+ .test-class {
304
+ border-bottom: 1px solid #eee;
305
+ }
306
+
307
+ .test-class:last-child {
308
+ border-bottom: none;
309
+ }
310
+
311
+ .test-class h2 {
312
+ background: #f8f9fa;
313
+ padding: 1rem 1.5rem;
314
+ margin: 0;
315
+ font-size: 1.25rem;
316
+ color: #2c3e50;
317
+ border-bottom: 1px solid #eee;
318
+ }
319
+
320
+ .test-method {
321
+ padding: 1.5rem;
322
+ border-bottom: 1px solid #f0f0f0;
323
+ }
324
+
325
+ .test-method:last-child {
326
+ border-bottom: none;
327
+ }
328
+
329
+ .test-method-header {
330
+ display: flex;
331
+ align-items: center;
332
+ margin-bottom: 1rem;
333
+ cursor: pointer;
334
+ gap: 0.75rem;
335
+ }
336
+
337
+ .test-method h3 {
338
+ margin: 0;
339
+ font-size: 1.1rem;
340
+ color: #34495e;
341
+ flex: 1;
342
+ }
343
+
344
+ .test-method.hidden {
345
+ display: none;
346
+ }
347
+
348
+ .test-class.hidden {
349
+ display: none;
350
+ }
351
+
352
+ .method-status {
353
+ padding: 0.25rem 0.75rem;
354
+ border-radius: 4px;
355
+ font-size: 0.75rem;
356
+ font-weight: 600;
357
+ text-transform: uppercase;
358
+ letter-spacing: 0.5px;
359
+ }
360
+
361
+ .method-status-passed {
362
+ background: #27ae60;
363
+ color: white;
364
+ }
365
+
366
+ .method-status-failed {
367
+ background: #e74c3c;
368
+ color: white;
369
+ }
370
+
371
+ .method-status-pending {
372
+ background: #f39c12;
373
+ color: white;
374
+ }
375
+
376
+ .expand-toggle {
377
+ background: none;
378
+ border: none;
379
+ cursor: pointer;
380
+ padding: 0.5rem;
381
+ border-radius: 4px;
382
+ transition: background-color 0.2s;
383
+ }
384
+
385
+ .expand-toggle:hover {
386
+ background-color: #f0f0f0;
387
+ }
388
+
389
+ .expand-icon {
390
+ font-size: 0.8rem;
391
+ color: #666;
392
+ }
393
+
394
+ .steps {
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 1rem;
398
+ transition: max-height 0.3s ease-out;
399
+ overflow: hidden;
400
+ }
401
+
402
+ .steps.collapsed {
403
+ max-height: 0;
404
+ margin: 0;
405
+ }
406
+
407
+ .step {
408
+ border: 1px solid #ddd;
409
+ border-radius: 6px;
410
+ padding: 1rem;
411
+ background: #fafafa;
412
+ }
413
+
414
+ .step.passed {
415
+ border-color: #27ae60;
416
+ background: #f8fff8;
417
+ }
418
+
419
+ .step.failed {
420
+ border-color: #e74c3c;
421
+ background: #fff8f8;
422
+ }
423
+
424
+ .step-header {
425
+ display: flex;
426
+ justify-content: space-between;
427
+ align-items: center;
428
+ margin-bottom: 0.5rem;
429
+ }
430
+
431
+ .step-name {
432
+ font-weight: 600;
433
+ color: #2c3e50;
434
+ }
435
+
436
+ .step-status {
437
+ padding: 0.25rem 0.5rem;
438
+ border-radius: 4px;
439
+ font-size: 0.8rem;
440
+ font-weight: 600;
441
+ text-transform: uppercase;
442
+ }
443
+
444
+ .step.passed .step-status {
445
+ background: #27ae60;
446
+ color: white;
447
+ }
448
+
449
+ .step.failed .step-status {
450
+ background: #e74c3c;
451
+ color: white;
452
+ }
453
+
454
+ .step-detail {
455
+ color: #666;
456
+ font-size: 0.9rem;
457
+ margin-bottom: 0.5rem;
458
+ }
459
+
460
+ .error-log {
461
+ margin-top: 1rem;
462
+ padding: 1rem;
463
+ background: #fff5f5;
464
+ border: 1px solid #fed7d7;
465
+ border-radius: 6px;
466
+ }
467
+
468
+ .error-log h4 {
469
+ color: #e53e3e;
470
+ margin: 0 0 0.5rem 0;
471
+ font-size: 0.9rem;
472
+ }
473
+
474
+ .error-log pre {
475
+ background: #2d3748;
476
+ color: #e2e8f0;
477
+ padding: 1rem;
478
+ border-radius: 4px;
479
+ overflow-x: auto;
480
+ font-size: 0.8rem;
481
+ line-height: 1.4;
482
+ margin: 0;
483
+ white-space: pre-wrap;
484
+ word-wrap: break-word;
485
+ }
486
+ CSS
487
+ end
488
+
489
+ def generate_javascript
490
+ <<~JS
491
+ function toggleTestMethod(safeId) {
492
+ const stepsContainer = document.getElementById('steps-' + safeId);
493
+ const button = document.querySelector('[onclick*="' + safeId + '"]');
494
+
495
+ if (stepsContainer && button) {
496
+ const icon = button.querySelector('.expand-icon');
497
+ if (icon) {
498
+ const isCollapsed = stepsContainer.classList.contains('collapsed');
499
+
500
+ if (isCollapsed) {
501
+ stepsContainer.classList.remove('collapsed');
502
+ icon.textContent = '▼';
503
+ } else {
504
+ stepsContainer.classList.add('collapsed');
505
+ icon.textContent = '▶';
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ // Search functionality
512
+ document.addEventListener('DOMContentLoaded', function() {
513
+ const searchInput = document.getElementById('searchInput');
514
+ if (!searchInput) return;
515
+
516
+ searchInput.addEventListener('input', function(e) {
517
+ const query = e.target.value.toLowerCase().trim();
518
+ const testMethods = document.querySelectorAll('.test-method');
519
+ const testClasses = document.querySelectorAll('.test-class');
520
+
521
+ // Determine if query is a status filter
522
+ const isStatusFilter = query === 'pass' || query === 'fail' ||
523
+ query === 'passed' || query === 'failed' ||
524
+ query === 'pending';
525
+
526
+ testMethods.forEach(function(method) {
527
+ const name = method.getAttribute('data-name') || '';
528
+ const status = method.getAttribute('data-status') || '';
529
+
530
+ let shouldShow = false;
531
+
532
+ if (!query) {
533
+ // No query - show all
534
+ shouldShow = true;
535
+ } else if (isStatusFilter) {
536
+ // Status filter - check status
537
+ shouldShow = (query === 'pass' && status === 'passed') ||
538
+ (query === 'fail' && status === 'failed') ||
539
+ query === status;
540
+ } else {
541
+ // Name filter - check if name contains query
542
+ shouldShow = name.includes(query);
543
+ }
544
+
545
+ if (shouldShow) {
546
+ method.classList.remove('hidden');
547
+ } else {
548
+ method.classList.add('hidden');
549
+ }
550
+ });
551
+
552
+ // Hide test classes if all methods are hidden
553
+ testClasses.forEach(function(testClass) {
554
+ const visibleMethods = testClass.querySelectorAll('.test-method:not(.hidden)');
555
+ if (visibleMethods.length === 0) {
556
+ testClass.classList.add('hidden');
557
+ } else {
558
+ testClass.classList.remove('hidden');
559
+ }
560
+ });
561
+ });
562
+ });
563
+ JS
564
+ end
565
+ end
566
+ end
567
+ 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>
@@ -13,11 +13,7 @@
13
13
  <div class="subtitle">Generated on <%= created_at.strftime('%B %d, %Y at %I:%M %p') %></div>
14
14
 
15
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>
16
+ <input type="text" id="searchInput" placeholder="Search tests or type 'pass' or 'fail' to filter..." class="search-input" autocomplete="off">
21
17
  </div>
22
18
  </div>
23
19
 
@@ -42,25 +38,20 @@
42
38
  <h2><%= test_class[:class_name] %></h2>
43
39
 
44
40
  <% test_class[:methods].each do |method| %>
45
- <div class="test-method">
41
+ <div class="test-method" data-status="<%= method[:status] %>" data-name="<%= method[:name].downcase %>">
46
42
  <div class="test-method-header">
47
- <button class="expand-toggle" onclick="toggleTestMethod('<%= method[:name] %>')">
43
+ <button class="expand-toggle" onclick="toggleTestMethod('<%= method[:safe_id] %>')">
48
44
  <span class="expand-icon">▶</span>
49
45
  </button>
50
46
  <h3><%= method[:name] %></h3>
47
+ <span class="method-status method-status-<%= method[:status] %>"><%= method[:status] %></span>
51
48
  </div>
52
- <div class="steps collapsed" id="steps-<%= method[:name] %>">
53
- <% method[:steps].each_with_index do |step, step_index| %>
49
+ <div class="steps collapsed" id="steps-<%= method[:safe_id] %>">
50
+ <% method[:steps].each do |step| %>
54
51
  <div class="step <%= step[:status] %>">
55
52
  <div class="step-header">
56
53
  <span class="step-name"><%= step[:name] %></span>
57
54
  <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
55
  </div>
65
56
  <div class="step-detail"><%= step[:detail] %></div>
66
57
 
@@ -70,14 +61,6 @@
70
61
  <pre><%= step[:error] %></pre>
71
62
  </div>
72
63
  <% 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
64
  </div>
82
65
  <% end %>
83
66
  </div>
@@ -1,3 +1,3 @@
1
- module Capydash
2
- VERSION = "0.2.0"
1
+ module CapyDash
2
+ VERSION = "0.2.2"
3
3
  end