capydash 0.2.4 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af3047672b2118bbe6b3eb83ae3d9149c96d4811d11cd6e66927bfb8acb88d02
4
- data.tar.gz: 25ab89fd8389339c24c4dc1d64d163d41331aed4f2aca9b268ddbcbcb96f8165
3
+ metadata.gz: 70d2bf9d0141c676e7685b951931458b7f224c210ae470de7a489b15d31018d8
4
+ data.tar.gz: b29754791193c6533d4c382585c0e7c2a489afe3a776eaec42d085c5fb601003
5
5
  SHA512:
6
- metadata.gz: f38f1587dcd12372431b5d5e39812a64aef58a1f5eff522a492ce85efd9317e97789d42195e96f682bf51aaced8e56ae99f6548611b6bbc885e56e91514c1edf
7
- data.tar.gz: 05b0a7453cfb5667bbdbb895685f3638e5d3b93df6a8dae0868b794bdd6e02222abd4947561a438f0b4702fff6bf5d1ac1eb94c22f962f3b0dc3ee64bcf52d9e
6
+ metadata.gz: c108d0f1310b218a43150e6d9421b19086353a23487af8b9fb08e479ededa947a4bb0fb35e6cd3acd6659c3afba008f698a665aa17d59bd948f3d1188300c3c7
7
+ data.tar.gz: b5e73a92af898a2ea0d1f4d876b61398084a018eb305e08427800152916ddc769d4d40966a97a552a64875d4672ce0d87969c90ec2bf93be4169448f740868ad
data/README.md CHANGED
@@ -1,143 +1,46 @@
1
1
  # CapyDash
2
2
 
3
- A minimal static HTML report generator for RSpec system tests. CapyDash automatically generates a clean, readable test report after your RSpec suite finishes.
3
+ Minimal, zero-config HTML report for your RSpec tests. Add the gem, run your tests, get a report.
4
4
 
5
- ## Features
5
+ ![CapyDash Report](docs/capydash-demo.gif)
6
6
 
7
- - ✅ **Automatic report generation** - No configuration needed
8
- - ✅ **RSpec system test support** - Works out of the box with `rspec-rails`
9
- - ✅ **Clean HTML reports** - Simple, readable test results
10
- - ✅ **Error details** - Full exception messages and backtraces
11
- - ✅ **Zero configuration** - Just add the gem and run your tests
7
+ ## Setup
12
8
 
13
- ## Installation
14
-
15
- Add to your Gemfile:
9
+ Add it to your Gemfile:
16
10
 
17
11
  ```ruby
18
- gem "capydash"
19
- gem "rspec-rails"
12
+ group :test do
13
+ gem "capydash"
14
+ end
20
15
  ```
21
16
 
22
- Then run:
23
-
24
- ```bash
25
- bundle install
26
- ```
17
+ Run `bundle install`. That's it — no configuration needed.
27
18
 
28
19
  ## Usage
29
20
 
30
- That's it! CapyDash automatically hooks into RSpec when it detects it. Just run your tests:
21
+ Run your tests as usual:
31
22
 
32
23
  ```bash
33
24
  bundle exec rspec
34
25
  ```
35
26
 
36
- After your test suite completes, CapyDash will automatically generate a report at:
27
+ After the suite finishes, open the generated report:
37
28
 
38
29
  ```
39
30
  capydash_report/index.html
40
31
  ```
41
32
 
42
- Open it in your browser to view the results.
43
-
44
- ## Example Test
45
-
46
- Here's an example RSpec system test:
47
-
48
- ```ruby
49
- require 'rails_helper'
50
-
51
- RSpec.describe "Homepage", type: :system do
52
- it "displays the welcome message" do
53
- visit "/"
54
- expect(page).to have_content("Welcome")
55
- end
56
-
57
- it "allows user to submit a form" do
58
- visit "/"
59
- fill_in "Your name", with: "Alice"
60
- click_button "Greet"
61
- expect(page).to have_content("Hello, Alice!")
62
- end
63
- end
64
- ```
65
-
66
- ## Report Features
67
-
68
- The generated report includes:
69
-
70
- - **Summary statistics** - Total, passed, and failed test counts
71
- - **Test grouping** - Tests organized by spec file
72
- - **Expandable test details** - Click to view error messages
73
- - **Error information** - Full exception messages and backtraces
74
- - **Clean design** - Simple, readable HTML layout
33
+ The report includes pass/fail counts, tests grouped by spec file, and expandable error details with backtraces.
75
34
 
76
35
  ## Requirements
77
36
 
37
+ - RSpec >= 3.0
78
38
  - Ruby 2.7+
79
- - RSpec 3.0+
80
- - Rails 6.0+ (for system tests)
81
39
 
82
40
  ## How It Works
83
41
 
84
- 1. CapyDash automatically detects when RSpec is present
85
- 2. Hooks into RSpec's `before(:suite)`, `after(:each)`, and `after(:suite)` callbacks
86
- 3. Collects test results in memory during the test run
87
- 4. Generates a static HTML report after all tests complete
88
- 5. Saves the report to `capydash_report/index.html`
89
-
90
- ## Troubleshooting
91
-
92
- ### Report not generated
93
-
94
- - Make sure you're running RSpec tests (not Minitest)
95
- - Ensure `rspec-rails` is in your Gemfile
96
- - Check that tests actually ran (no early exits)
97
-
98
- ### Tests not appearing in report
99
-
100
- - Verify you're using RSpec system tests (`type: :system`)
101
- - Make sure the test suite completed (not interrupted)
102
-
103
- ### Report shows old results
104
-
105
- - Delete the `capydash_report` directory and run tests again
106
- - The report is regenerated on each test run
107
-
108
- ## Development
109
-
110
- ### Running Tests
111
-
112
- ```bash
113
- # In a Rails app with RSpec
114
- bundle exec rspec
115
- ```
116
-
117
- ### Building the Gem
118
-
119
- ```bash
120
- gem build capydash.gemspec
121
- ```
122
-
123
- ### Publishing
124
-
125
- ```bash
126
- gem push capydash-0.2.0.gem
127
- ```
42
+ CapyDash hooks into RSpec automatically via `before(:suite)`, `after(:each)`, and `after(:suite)` callbacks. It collects results in memory during the run and writes a static HTML report to `capydash_report/` when the suite completes. No server, no database, no config files.
128
43
 
129
44
  ## License
130
45
 
131
46
  MIT
132
-
133
- ## Contributing
134
-
135
- 1. Fork the repository
136
- 2. Create a feature branch
137
- 3. Make your changes
138
- 4. Test with RSpec
139
- 5. Submit a pull request
140
-
141
- ---
142
-
143
- **Note:** CapyDash is a minimal MVP focused solely on RSpec system test reporting. It does not support Minitest, configuration DSLs, local servers, or screenshots. For a simple, zero-configuration test reporting solution, CapyDash is perfect.
data/capydash.gemspec CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
7
7
  spec.authors = ["Damon Clark"]
8
8
  spec.email = ["dclark312@gmail.com"]
9
9
 
10
- spec.summary = "Minimal static HTML report generator for RSpec system tests"
11
- spec.description = "CapyDash automatically generates clean, readable HTML test reports after your RSpec suite finishes. Zero configuration required."
10
+ spec.summary = "Minimal static HTML report generator for RSpec and Minitest system tests"
11
+ spec.description = "CapyDash automatically generates clean, readable HTML test reports after your RSpec or Minitest suite finishes. Zero configuration required."
12
12
  spec.homepage = "https://github.com/damonclark/capydash"
13
13
  spec.license = "MIT"
14
14
 
@@ -16,10 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.files = Dir["lib/**/*", "README.md", "LICENSE*", "*.gemspec"]
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- # Dependencies
20
- spec.add_runtime_dependency "rspec", ">= 3.0"
21
-
22
- # Development dependencies
19
+ # Development dependencies (no runtime deps — works with RSpec or Minitest)
23
20
  spec.add_development_dependency "rspec-rails", "~> 6.0"
24
21
  spec.add_development_dependency "rails", ">= 6.0"
25
22
 
@@ -0,0 +1,340 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
9
+ line-height: 1.6;
10
+ color: #333;
11
+ background-color: #f8f9fa;
12
+ }
13
+
14
+ .container {
15
+ max-width: 1400px;
16
+ margin: 0 auto;
17
+ padding: 1rem;
18
+ }
19
+
20
+ .header {
21
+ background: white;
22
+ padding: 1.5rem;
23
+ border-radius: 8px;
24
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
25
+ margin-bottom: 1.5rem;
26
+ }
27
+
28
+ .header h1 {
29
+ font-size: 2rem;
30
+ margin-bottom: 0.5rem;
31
+ color: #2c3e50;
32
+ }
33
+
34
+ .header .subtitle {
35
+ color: #666;
36
+ font-size: 0.9rem;
37
+ margin-bottom: 1rem;
38
+ }
39
+
40
+ .search-container {
41
+ margin-top: 1rem;
42
+ }
43
+
44
+ .search-input {
45
+ width: 100%;
46
+ padding: 0.75rem 1rem;
47
+ border: 2px solid #ddd;
48
+ border-radius: 6px;
49
+ font-size: 1rem;
50
+ transition: border-color 0.2s;
51
+ }
52
+
53
+ .search-input:focus {
54
+ outline: none;
55
+ border-color: #3498db;
56
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
57
+ }
58
+
59
+ .summary {
60
+ display: grid;
61
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
62
+ gap: 1rem;
63
+ margin-bottom: 1.5rem;
64
+ }
65
+
66
+ .summary-card {
67
+ background: white;
68
+ padding: 1.5rem;
69
+ border-radius: 8px;
70
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
71
+ text-align: center;
72
+ }
73
+
74
+ .summary-card .number {
75
+ font-size: 2.5rem;
76
+ font-weight: bold;
77
+ margin-bottom: 0.5rem;
78
+ }
79
+
80
+ .summary-card.total .number {
81
+ color: #3498db;
82
+ }
83
+
84
+ .summary-card.passed .number {
85
+ color: #27ae60;
86
+ }
87
+
88
+ .summary-card.failed .number {
89
+ color: #e74c3c;
90
+ }
91
+
92
+ .summary-card .label {
93
+ color: #666;
94
+ font-size: 0.9rem;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.5px;
97
+ }
98
+
99
+ .test-results {
100
+ background: white;
101
+ border-radius: 8px;
102
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
103
+ overflow: hidden;
104
+ }
105
+
106
+ .test-class {
107
+ border-bottom: 1px solid #eee;
108
+ }
109
+
110
+ .test-class:last-child {
111
+ border-bottom: none;
112
+ }
113
+
114
+ .test-class.hidden {
115
+ display: none;
116
+ }
117
+
118
+ .test-class h2 {
119
+ background: #f8f9fa;
120
+ padding: 1rem 1.5rem;
121
+ margin: 0;
122
+ font-size: 1.25rem;
123
+ color: #2c3e50;
124
+ border-bottom: 1px solid #eee;
125
+ }
126
+
127
+ .test-method {
128
+ padding: 1.5rem;
129
+ border-bottom: 1px solid #f0f0f0;
130
+ }
131
+
132
+ .test-method:last-child {
133
+ border-bottom: none;
134
+ }
135
+
136
+ .test-method.hidden {
137
+ display: none;
138
+ }
139
+
140
+ .test-method-header {
141
+ display: flex;
142
+ align-items: center;
143
+ margin-bottom: 1rem;
144
+ cursor: pointer;
145
+ gap: 0.75rem;
146
+ }
147
+
148
+ .test-method h3 {
149
+ margin: 0;
150
+ font-size: 1.1rem;
151
+ color: #34495e;
152
+ flex: 1;
153
+ }
154
+
155
+ .method-status {
156
+ display: inline-flex;
157
+ align-items: center;
158
+ padding: 0.35rem 0.85rem;
159
+ border-radius: 4px;
160
+ font-size: 0.75rem;
161
+ font-weight: 600;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.5px;
164
+ white-space: nowrap;
165
+ min-width: 60px;
166
+ justify-content: center;
167
+ }
168
+
169
+ .method-status-passed {
170
+ background-color: #27ae60;
171
+ color: white;
172
+ }
173
+
174
+ .method-status-failed {
175
+ background-color: #e74c3c;
176
+ color: white;
177
+ }
178
+
179
+ .method-status-pending {
180
+ background-color: #f39c12;
181
+ color: white;
182
+ }
183
+
184
+ .expand-toggle {
185
+ background: none;
186
+ border: none;
187
+ cursor: pointer;
188
+ padding: 0.5rem;
189
+ border-radius: 4px;
190
+ transition: background-color 0.2s;
191
+ }
192
+
193
+ .expand-toggle:hover {
194
+ background-color: #f0f0f0;
195
+ }
196
+
197
+ .expand-icon {
198
+ font-size: 0.8rem;
199
+ color: #666;
200
+ }
201
+
202
+ .steps {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 1rem;
206
+ transition: max-height 0.3s ease-out;
207
+ overflow: hidden;
208
+ }
209
+
210
+ .steps.collapsed {
211
+ max-height: 0;
212
+ margin: 0;
213
+ }
214
+
215
+ .step {
216
+ border: 1px solid #ddd;
217
+ border-radius: 6px;
218
+ padding: 1rem;
219
+ background: #fafafa;
220
+ }
221
+
222
+ .step.passed {
223
+ border-color: #27ae60;
224
+ background: #f8fff8;
225
+ }
226
+
227
+ .step.failed {
228
+ border-color: #e74c3c;
229
+ background: #fff8f8;
230
+ }
231
+
232
+ .step-header {
233
+ display: flex;
234
+ justify-content: space-between;
235
+ align-items: center;
236
+ margin-bottom: 0.5rem;
237
+ }
238
+
239
+ .step-name {
240
+ font-weight: 600;
241
+ color: #2c3e50;
242
+ }
243
+
244
+ .step-status {
245
+ padding: 0.25rem 0.5rem;
246
+ border-radius: 4px;
247
+ font-size: 0.8rem;
248
+ font-weight: 600;
249
+ text-transform: uppercase;
250
+ }
251
+
252
+ .step.passed .step-status {
253
+ background: #27ae60;
254
+ color: white;
255
+ }
256
+
257
+ .step.failed .step-status {
258
+ background: #e74c3c;
259
+ color: white;
260
+ }
261
+
262
+ .step-detail {
263
+ color: #666;
264
+ font-size: 0.9rem;
265
+ margin-bottom: 0.5rem;
266
+ }
267
+
268
+ .error-log {
269
+ margin-top: 1rem;
270
+ padding: 1rem;
271
+ background: #fff5f5;
272
+ border: 1px solid #fed7d7;
273
+ border-radius: 6px;
274
+ }
275
+
276
+ .error-log h4 {
277
+ color: #e53e3e;
278
+ margin: 0 0 0.5rem 0;
279
+ font-size: 0.9rem;
280
+ }
281
+
282
+ .error-log pre {
283
+ background: #2d3748;
284
+ color: #e2e8f0;
285
+ padding: 1rem;
286
+ border-radius: 4px;
287
+ overflow-x: auto;
288
+ font-size: 0.8rem;
289
+ line-height: 1.4;
290
+ margin: 0;
291
+ white-space: pre-wrap;
292
+ word-wrap: break-word;
293
+ }
294
+
295
+ .screenshot-container {
296
+ margin-top: 1rem;
297
+ padding: 1rem;
298
+ background: #fff5f5;
299
+ border: 1px solid #fed7d7;
300
+ border-radius: 6px;
301
+ cursor: pointer;
302
+ }
303
+
304
+ .screenshot-container h4 {
305
+ color: #e53e3e;
306
+ margin: 0 0 0.5rem 0;
307
+ font-size: 0.9rem;
308
+ }
309
+
310
+ .screenshot-container img {
311
+ max-width: 100%;
312
+ max-height: 300px;
313
+ border-radius: 4px;
314
+ border: 1px solid #ddd;
315
+ }
316
+
317
+ .lightbox {
318
+ display: none;
319
+ position: fixed;
320
+ top: 0;
321
+ left: 0;
322
+ width: 100%;
323
+ height: 100%;
324
+ background: rgba(0, 0, 0, 0.85);
325
+ z-index: 9999;
326
+ justify-content: center;
327
+ align-items: center;
328
+ cursor: pointer;
329
+ }
330
+
331
+ .lightbox.active {
332
+ display: flex;
333
+ }
334
+
335
+ .lightbox img {
336
+ max-width: 90%;
337
+ max-height: 90%;
338
+ border-radius: 6px;
339
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
340
+ }
@@ -0,0 +1,94 @@
1
+ function openLightbox(src) {
2
+ var lightbox = document.getElementById('screenshotLightbox');
3
+ var img = document.getElementById('lightboxImage');
4
+ if (lightbox && img) {
5
+ img.src = src;
6
+ lightbox.classList.add('active');
7
+ }
8
+ }
9
+
10
+ function closeLightbox() {
11
+ var lightbox = document.getElementById('screenshotLightbox');
12
+ if (lightbox) {
13
+ lightbox.classList.remove('active');
14
+ }
15
+ }
16
+
17
+ document.addEventListener('keydown', function(e) {
18
+ if (e.key === 'Escape') {
19
+ closeLightbox();
20
+ }
21
+ });
22
+
23
+ function toggleTestMethod(safeId) {
24
+ const stepsContainer = document.getElementById('steps-' + safeId);
25
+ const button = document.querySelector('[onclick*="' + safeId + '"]');
26
+
27
+ if (stepsContainer && button) {
28
+ const icon = button.querySelector('.expand-icon');
29
+ if (icon) {
30
+ const isCollapsed = stepsContainer.classList.contains('collapsed');
31
+
32
+ if (isCollapsed) {
33
+ stepsContainer.classList.remove('collapsed');
34
+ icon.textContent = '\u25BC';
35
+ } else {
36
+ stepsContainer.classList.add('collapsed');
37
+ icon.textContent = '\u25B6';
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ // Search functionality
44
+ document.addEventListener('DOMContentLoaded', function() {
45
+ const searchInput = document.getElementById('searchInput');
46
+ if (!searchInput) return;
47
+
48
+ searchInput.addEventListener('input', function(e) {
49
+ const query = e.target.value.toLowerCase().trim();
50
+ const testMethods = document.querySelectorAll('.test-method');
51
+ const testClasses = document.querySelectorAll('.test-class');
52
+
53
+ // Determine if query is a status filter
54
+ const isStatusFilter = query === 'pass' || query === 'fail' ||
55
+ query === 'passed' || query === 'failed' ||
56
+ query === 'pending';
57
+
58
+ testMethods.forEach(function(method) {
59
+ const name = method.getAttribute('data-name') || '';
60
+ const status = method.getAttribute('data-status') || '';
61
+
62
+ let shouldShow = false;
63
+
64
+ if (!query) {
65
+ // No query - show all
66
+ shouldShow = true;
67
+ } else if (isStatusFilter) {
68
+ // Status filter - check status
69
+ shouldShow = (query === 'pass' && status === 'passed') ||
70
+ (query === 'fail' && status === 'failed') ||
71
+ query === status;
72
+ } else {
73
+ // Name filter - check if name contains query
74
+ shouldShow = name.includes(query);
75
+ }
76
+
77
+ if (shouldShow) {
78
+ method.classList.remove('hidden');
79
+ } else {
80
+ method.classList.add('hidden');
81
+ }
82
+ });
83
+
84
+ // Hide test classes if all methods are hidden
85
+ testClasses.forEach(function(testClass) {
86
+ const visibleMethods = testClass.querySelectorAll('.test-method:not(.hidden)');
87
+ if (visibleMethods.length === 0) {
88
+ testClass.classList.add('hidden');
89
+ } else {
90
+ testClass.classList.remove('hidden');
91
+ }
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,72 @@
1
+ require 'capydash/reporter'
2
+
3
+ module CapyDash
4
+ module Minitest
5
+ class Reporter < ::Minitest::AbstractReporter
6
+ include CapyDash::Reporter
7
+
8
+ def start
9
+ start_run
10
+ end
11
+
12
+ def record(result)
13
+ return unless @started_at
14
+
15
+ status = if result.skipped?
16
+ 'pending'
17
+ elsif result.passed?
18
+ 'passed'
19
+ else
20
+ 'failed'
21
+ end
22
+
23
+ error_message = nil
24
+ if result.failure
25
+ error_message = format_exception(result.failure)
26
+ end
27
+
28
+ screenshot_path = nil
29
+ if status == 'failed'
30
+ # Rails system tests save screenshots before teardown to tmp/capybara/.
31
+ # By the time the reporter's record() runs, the session is torn down,
32
+ # so we look for the Rails-generated screenshot first.
33
+ screenshot_path = find_rails_screenshot(result.name) || capture_screenshot
34
+ end
35
+
36
+ class_name = result.klass || 'UnknownTest'
37
+ method_name = result.name.to_s.sub(/\Atest_/, '').tr('_', ' ')
38
+
39
+ location = nil
40
+ if result.respond_to?(:source_location) && result.source_location
41
+ location = result.source_location.join(':')
42
+ end
43
+
44
+ record_result({
45
+ class_name: class_name,
46
+ method_name: method_name,
47
+ status: status,
48
+ error: error_message,
49
+ location: location,
50
+ screenshot_path: screenshot_path
51
+ })
52
+ end
53
+
54
+ def report
55
+ generate_report
56
+ end
57
+
58
+ def passed?
59
+ true
60
+ end
61
+
62
+ private
63
+
64
+ def find_rails_screenshot(test_name)
65
+ path = File.join(Dir.pwd, "tmp", "capybara", "failures_#{test_name}.png")
66
+ File.exist?(path) ? path : nil
67
+ rescue
68
+ nil
69
+ end
70
+ end
71
+ end
72
+ end