capydash 0.2.3 → 0.2.5
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 +4 -4
- data/README.md +13 -110
- data/lib/capydash/assets/dashboard.css +293 -0
- data/lib/capydash/assets/dashboard.js +72 -0
- data/lib/capydash/rspec.rb +46 -372
- data/lib/capydash/templates/report.html.erb +14 -12
- data/lib/capydash/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc3c0c3e8b55e78af53e8f1a8a34f44165da41b6149dec134cd2a573ba959a98
|
|
4
|
+
data.tar.gz: 4e3dbca12dc81a9914e418092def3c7a58a0bb1dfcad61d9b8d8056c99b2c063
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 578279a43a188c65e710d163fb30936f96ceee8d4a4803408a25683d6547695c48250f960d87f63179206b3e1345fe52290f40b184805a9b8c3e004205c05f09
|
|
7
|
+
data.tar.gz: 72218c84707dfd0e7b7275b87287862e45cf2bfa629169cea372be719a6314b63350a427831ad5016478fe443d3c680385b5cc5f879948e11048cdf494ba1f3f
|
data/README.md
CHANGED
|
@@ -1,143 +1,46 @@
|
|
|
1
1
|
# CapyDash
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimal, zero-config HTML report for your RSpec tests. Add the gem, run your tests, get a report.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
Add to your Gemfile:
|
|
9
|
+
Add it to your Gemfile:
|
|
16
10
|
|
|
17
11
|
```ruby
|
|
18
|
-
|
|
19
|
-
gem "
|
|
12
|
+
group :test do
|
|
13
|
+
gem "capydash"
|
|
14
|
+
end
|
|
20
15
|
```
|
|
21
16
|
|
|
22
|
-
|
|
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
|
-
|
|
21
|
+
Run your tests as usual:
|
|
31
22
|
|
|
32
23
|
```bash
|
|
33
24
|
bundle exec rspec
|
|
34
25
|
```
|
|
35
26
|
|
|
36
|
-
After
|
|
27
|
+
After the suite finishes, open the generated report:
|
|
37
28
|
|
|
38
29
|
```
|
|
39
30
|
capydash_report/index.html
|
|
40
31
|
```
|
|
41
32
|
|
|
42
|
-
|
|
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
|
-
|
|
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.
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function toggleTestMethod(safeId) {
|
|
2
|
+
const stepsContainer = document.getElementById('steps-' + safeId);
|
|
3
|
+
const button = document.querySelector('[onclick*="' + safeId + '"]');
|
|
4
|
+
|
|
5
|
+
if (stepsContainer && button) {
|
|
6
|
+
const icon = button.querySelector('.expand-icon');
|
|
7
|
+
if (icon) {
|
|
8
|
+
const isCollapsed = stepsContainer.classList.contains('collapsed');
|
|
9
|
+
|
|
10
|
+
if (isCollapsed) {
|
|
11
|
+
stepsContainer.classList.remove('collapsed');
|
|
12
|
+
icon.textContent = '\u25BC';
|
|
13
|
+
} else {
|
|
14
|
+
stepsContainer.classList.add('collapsed');
|
|
15
|
+
icon.textContent = '\u25B6';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Search functionality
|
|
22
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
23
|
+
const searchInput = document.getElementById('searchInput');
|
|
24
|
+
if (!searchInput) return;
|
|
25
|
+
|
|
26
|
+
searchInput.addEventListener('input', function(e) {
|
|
27
|
+
const query = e.target.value.toLowerCase().trim();
|
|
28
|
+
const testMethods = document.querySelectorAll('.test-method');
|
|
29
|
+
const testClasses = document.querySelectorAll('.test-class');
|
|
30
|
+
|
|
31
|
+
// Determine if query is a status filter
|
|
32
|
+
const isStatusFilter = query === 'pass' || query === 'fail' ||
|
|
33
|
+
query === 'passed' || query === 'failed' ||
|
|
34
|
+
query === 'pending';
|
|
35
|
+
|
|
36
|
+
testMethods.forEach(function(method) {
|
|
37
|
+
const name = method.getAttribute('data-name') || '';
|
|
38
|
+
const status = method.getAttribute('data-status') || '';
|
|
39
|
+
|
|
40
|
+
let shouldShow = false;
|
|
41
|
+
|
|
42
|
+
if (!query) {
|
|
43
|
+
// No query - show all
|
|
44
|
+
shouldShow = true;
|
|
45
|
+
} else if (isStatusFilter) {
|
|
46
|
+
// Status filter - check status
|
|
47
|
+
shouldShow = (query === 'pass' && status === 'passed') ||
|
|
48
|
+
(query === 'fail' && status === 'failed') ||
|
|
49
|
+
query === status;
|
|
50
|
+
} else {
|
|
51
|
+
// Name filter - check if name contains query
|
|
52
|
+
shouldShow = name.includes(query);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (shouldShow) {
|
|
56
|
+
method.classList.remove('hidden');
|
|
57
|
+
} else {
|
|
58
|
+
method.classList.add('hidden');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Hide test classes if all methods are hidden
|
|
63
|
+
testClasses.forEach(function(testClass) {
|
|
64
|
+
const visibleMethods = testClass.querySelectorAll('.test-method:not(.hidden)');
|
|
65
|
+
if (visibleMethods.length === 0) {
|
|
66
|
+
testClass.classList.add('hidden');
|
|
67
|
+
} else {
|
|
68
|
+
testClass.classList.remove('hidden');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
data/lib/capydash/rspec.rb
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
require 'time'
|
|
2
2
|
require 'fileutils'
|
|
3
3
|
require 'erb'
|
|
4
|
+
require 'cgi'
|
|
4
5
|
|
|
5
6
|
module CapyDash
|
|
7
|
+
class ReportData
|
|
8
|
+
attr_reader :processed_tests, :created_at, :total_tests, :passed_tests, :failed_tests
|
|
9
|
+
|
|
10
|
+
def initialize(processed_tests:, created_at:, total_tests:, passed_tests:, failed_tests:)
|
|
11
|
+
@processed_tests = processed_tests
|
|
12
|
+
@created_at = created_at
|
|
13
|
+
@total_tests = total_tests
|
|
14
|
+
@passed_tests = passed_tests
|
|
15
|
+
@failed_tests = failed_tests
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def h(text)
|
|
19
|
+
CGI.escapeHTML(text.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def get_binding
|
|
23
|
+
binding
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
6
27
|
module RSpec
|
|
7
28
|
class << self
|
|
8
29
|
# Public method: Called from RSpec before(:suite) hook
|
|
@@ -23,8 +44,7 @@ module CapyDash
|
|
|
23
44
|
error_message = format_exception(execution_result.exception)
|
|
24
45
|
end
|
|
25
46
|
|
|
26
|
-
|
|
27
|
-
class_name = extract_class_name(file_path)
|
|
47
|
+
class_name = extract_class_name(example)
|
|
28
48
|
|
|
29
49
|
@results << {
|
|
30
50
|
class_name: class_name,
|
|
@@ -128,7 +148,6 @@ module CapyDash
|
|
|
128
148
|
end
|
|
129
149
|
|
|
130
150
|
def normalize_status(status)
|
|
131
|
-
# Normalize RSpec status symbols to strings
|
|
132
151
|
case status
|
|
133
152
|
when :passed, 'passed'
|
|
134
153
|
'passed'
|
|
@@ -141,11 +160,20 @@ module CapyDash
|
|
|
141
160
|
end
|
|
142
161
|
end
|
|
143
162
|
|
|
144
|
-
def extract_class_name(
|
|
145
|
-
|
|
163
|
+
def extract_class_name(example)
|
|
164
|
+
group = example.metadata[:example_group]
|
|
165
|
+
while group && group[:parent_example_group]
|
|
166
|
+
group = group[:parent_example_group]
|
|
167
|
+
end
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
if group && group[:description] && !group[:description].to_s.empty?
|
|
170
|
+
group[:description].to_s
|
|
171
|
+
else
|
|
172
|
+
file_path = example.metadata[:file_path] || ''
|
|
173
|
+
return 'UnknownSpec' if file_path.empty?
|
|
174
|
+
filename = File.basename(file_path, '.rb')
|
|
175
|
+
filename.split('_').map(&:capitalize).join('')
|
|
176
|
+
end
|
|
149
177
|
end
|
|
150
178
|
|
|
151
179
|
def format_exception(exception)
|
|
@@ -174,377 +202,23 @@ module CapyDash
|
|
|
174
202
|
template = File.read(template_path)
|
|
175
203
|
erb = ERB.new(template)
|
|
176
204
|
|
|
177
|
-
|
|
205
|
+
report_data = CapyDash::ReportData.new(
|
|
206
|
+
processed_tests: processed_tests,
|
|
207
|
+
created_at: created_at,
|
|
208
|
+
total_tests: total_tests,
|
|
209
|
+
passed_tests: passed_tests,
|
|
210
|
+
failed_tests: failed_tests
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
erb.result(report_data.get_binding)
|
|
178
214
|
end
|
|
179
215
|
|
|
180
216
|
def generate_css
|
|
181
|
-
|
|
182
|
-
* {
|
|
183
|
-
margin: 0;
|
|
184
|
-
padding: 0;
|
|
185
|
-
box-sizing: border-box;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
body {
|
|
189
|
-
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
190
|
-
line-height: 1.6;
|
|
191
|
-
color: #333;
|
|
192
|
-
background-color: #f8f9fa;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.container {
|
|
196
|
-
max-width: 1400px;
|
|
197
|
-
margin: 0 auto;
|
|
198
|
-
padding: 1rem;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.header {
|
|
202
|
-
background: white;
|
|
203
|
-
padding: 1.5rem;
|
|
204
|
-
border-radius: 8px;
|
|
205
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
206
|
-
margin-bottom: 1.5rem;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.header h1 {
|
|
210
|
-
font-size: 2rem;
|
|
211
|
-
margin-bottom: 0.5rem;
|
|
212
|
-
color: #2c3e50;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.header .subtitle {
|
|
216
|
-
color: #666;
|
|
217
|
-
font-size: 0.9rem;
|
|
218
|
-
margin-bottom: 1rem;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.search-container {
|
|
222
|
-
margin-top: 1rem;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
.search-input {
|
|
226
|
-
width: 100%;
|
|
227
|
-
padding: 0.75rem 1rem;
|
|
228
|
-
border: 2px solid #ddd;
|
|
229
|
-
border-radius: 6px;
|
|
230
|
-
font-size: 1rem;
|
|
231
|
-
transition: border-color 0.2s;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
.search-input:focus {
|
|
235
|
-
outline: none;
|
|
236
|
-
border-color: #3498db;
|
|
237
|
-
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.summary {
|
|
241
|
-
display: grid;
|
|
242
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
243
|
-
gap: 1rem;
|
|
244
|
-
margin-bottom: 1.5rem;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.summary-card {
|
|
248
|
-
background: white;
|
|
249
|
-
padding: 1.5rem;
|
|
250
|
-
border-radius: 8px;
|
|
251
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
252
|
-
text-align: center;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
.summary-card .number {
|
|
256
|
-
font-size: 2.5rem;
|
|
257
|
-
font-weight: bold;
|
|
258
|
-
margin-bottom: 0.5rem;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.summary-card.total .number {
|
|
262
|
-
color: #3498db;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
.summary-card.passed .number {
|
|
266
|
-
color: #27ae60;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.summary-card.failed .number {
|
|
270
|
-
color: #e74c3c;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.summary-card .label {
|
|
274
|
-
color: #666;
|
|
275
|
-
font-size: 0.9rem;
|
|
276
|
-
text-transform: uppercase;
|
|
277
|
-
letter-spacing: 0.5px;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.test-results {
|
|
281
|
-
background: white;
|
|
282
|
-
border-radius: 8px;
|
|
283
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
284
|
-
overflow: hidden;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.test-class {
|
|
288
|
-
border-bottom: 1px solid #eee;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.test-class:last-child {
|
|
292
|
-
border-bottom: none;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
.test-class.hidden {
|
|
296
|
-
display: none;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.test-class h2 {
|
|
300
|
-
background: #f8f9fa;
|
|
301
|
-
padding: 1rem 1.5rem;
|
|
302
|
-
margin: 0;
|
|
303
|
-
font-size: 1.25rem;
|
|
304
|
-
color: #2c3e50;
|
|
305
|
-
border-bottom: 1px solid #eee;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
.test-method {
|
|
309
|
-
padding: 1.5rem;
|
|
310
|
-
border-bottom: 1px solid #f0f0f0;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
.test-method:last-child {
|
|
314
|
-
border-bottom: none;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
.test-method.hidden {
|
|
318
|
-
display: none;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
.test-method-header {
|
|
322
|
-
display: flex;
|
|
323
|
-
align-items: center;
|
|
324
|
-
margin-bottom: 1rem;
|
|
325
|
-
cursor: pointer;
|
|
326
|
-
gap: 0.75rem;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
.test-method h3 {
|
|
330
|
-
margin: 0;
|
|
331
|
-
font-size: 1.1rem;
|
|
332
|
-
color: #34495e;
|
|
333
|
-
flex: 1;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.method-status {
|
|
337
|
-
padding: 0.25rem 0.75rem;
|
|
338
|
-
border-radius: 4px;
|
|
339
|
-
font-size: 0.75rem;
|
|
340
|
-
font-weight: 600;
|
|
341
|
-
text-transform: uppercase;
|
|
342
|
-
letter-spacing: 0.5px;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
.method-status-passed {
|
|
346
|
-
background: #27ae60;
|
|
347
|
-
color: white;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.method-status-failed {
|
|
351
|
-
background: #e74c3c;
|
|
352
|
-
color: white;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.method-status-pending {
|
|
356
|
-
background: #f39c12;
|
|
357
|
-
color: white;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
.expand-toggle {
|
|
361
|
-
background: none;
|
|
362
|
-
border: none;
|
|
363
|
-
cursor: pointer;
|
|
364
|
-
padding: 0.5rem;
|
|
365
|
-
border-radius: 4px;
|
|
366
|
-
transition: background-color 0.2s;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
.expand-toggle:hover {
|
|
370
|
-
background-color: #f0f0f0;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.expand-icon {
|
|
374
|
-
font-size: 0.8rem;
|
|
375
|
-
color: #666;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
.steps {
|
|
379
|
-
display: flex;
|
|
380
|
-
flex-direction: column;
|
|
381
|
-
gap: 1rem;
|
|
382
|
-
transition: max-height 0.3s ease-out;
|
|
383
|
-
overflow: hidden;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
.steps.collapsed {
|
|
387
|
-
max-height: 0;
|
|
388
|
-
margin: 0;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.step {
|
|
392
|
-
border: 1px solid #ddd;
|
|
393
|
-
border-radius: 6px;
|
|
394
|
-
padding: 1rem;
|
|
395
|
-
background: #fafafa;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
.step.passed {
|
|
399
|
-
border-color: #27ae60;
|
|
400
|
-
background: #f8fff8;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
.step.failed {
|
|
404
|
-
border-color: #e74c3c;
|
|
405
|
-
background: #fff8f8;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.step-header {
|
|
409
|
-
display: flex;
|
|
410
|
-
justify-content: space-between;
|
|
411
|
-
align-items: center;
|
|
412
|
-
margin-bottom: 0.5rem;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.step-name {
|
|
416
|
-
font-weight: 600;
|
|
417
|
-
color: #2c3e50;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
.step-status {
|
|
421
|
-
padding: 0.25rem 0.5rem;
|
|
422
|
-
border-radius: 4px;
|
|
423
|
-
font-size: 0.8rem;
|
|
424
|
-
font-weight: 600;
|
|
425
|
-
text-transform: uppercase;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
.step.passed .step-status {
|
|
429
|
-
background: #27ae60;
|
|
430
|
-
color: white;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.step.failed .step-status {
|
|
434
|
-
background: #e74c3c;
|
|
435
|
-
color: white;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
.step-detail {
|
|
439
|
-
color: #666;
|
|
440
|
-
font-size: 0.9rem;
|
|
441
|
-
margin-bottom: 0.5rem;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
.error-log {
|
|
445
|
-
margin-top: 1rem;
|
|
446
|
-
padding: 1rem;
|
|
447
|
-
background: #fff5f5;
|
|
448
|
-
border: 1px solid #fed7d7;
|
|
449
|
-
border-radius: 6px;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
.error-log h4 {
|
|
453
|
-
color: #e53e3e;
|
|
454
|
-
margin: 0 0 0.5rem 0;
|
|
455
|
-
font-size: 0.9rem;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
.error-log pre {
|
|
459
|
-
background: #2d3748;
|
|
460
|
-
color: #e2e8f0;
|
|
461
|
-
padding: 1rem;
|
|
462
|
-
border-radius: 4px;
|
|
463
|
-
overflow-x: auto;
|
|
464
|
-
font-size: 0.8rem;
|
|
465
|
-
line-height: 1.4;
|
|
466
|
-
margin: 0;
|
|
467
|
-
white-space: pre-wrap;
|
|
468
|
-
word-wrap: break-word;
|
|
469
|
-
}
|
|
470
|
-
CSS
|
|
217
|
+
File.read(File.join(__dir__, 'assets', 'dashboard.css'))
|
|
471
218
|
end
|
|
472
219
|
|
|
473
220
|
def generate_javascript
|
|
474
|
-
|
|
475
|
-
function toggleTestMethod(safeId) {
|
|
476
|
-
const stepsContainer = document.getElementById('steps-' + safeId);
|
|
477
|
-
const button = document.querySelector('[onclick*="' + safeId + '"]');
|
|
478
|
-
|
|
479
|
-
if (stepsContainer && button) {
|
|
480
|
-
const icon = button.querySelector('.expand-icon');
|
|
481
|
-
if (icon) {
|
|
482
|
-
const isCollapsed = stepsContainer.classList.contains('collapsed');
|
|
483
|
-
|
|
484
|
-
if (isCollapsed) {
|
|
485
|
-
stepsContainer.classList.remove('collapsed');
|
|
486
|
-
icon.textContent = '▼';
|
|
487
|
-
} else {
|
|
488
|
-
stepsContainer.classList.add('collapsed');
|
|
489
|
-
icon.textContent = '▶';
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Search functionality
|
|
496
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
497
|
-
const searchInput = document.getElementById('searchInput');
|
|
498
|
-
if (!searchInput) return;
|
|
499
|
-
|
|
500
|
-
searchInput.addEventListener('input', function(e) {
|
|
501
|
-
const query = e.target.value.toLowerCase().trim();
|
|
502
|
-
const testMethods = document.querySelectorAll('.test-method');
|
|
503
|
-
const testClasses = document.querySelectorAll('.test-class');
|
|
504
|
-
|
|
505
|
-
// Determine if query is a status filter
|
|
506
|
-
const isStatusFilter = query === 'pass' || query === 'fail' ||
|
|
507
|
-
query === 'passed' || query === 'failed' ||
|
|
508
|
-
query === 'pending';
|
|
509
|
-
|
|
510
|
-
testMethods.forEach(function(method) {
|
|
511
|
-
const name = method.getAttribute('data-name') || '';
|
|
512
|
-
const status = method.getAttribute('data-status') || '';
|
|
513
|
-
|
|
514
|
-
let shouldShow = false;
|
|
515
|
-
|
|
516
|
-
if (!query) {
|
|
517
|
-
// No query - show all
|
|
518
|
-
shouldShow = true;
|
|
519
|
-
} else if (isStatusFilter) {
|
|
520
|
-
// Status filter - check status
|
|
521
|
-
shouldShow = (query === 'pass' && status === 'passed') ||
|
|
522
|
-
(query === 'fail' && status === 'failed') ||
|
|
523
|
-
query === status;
|
|
524
|
-
} else {
|
|
525
|
-
// Name filter - check if name contains query
|
|
526
|
-
shouldShow = name.includes(query);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (shouldShow) {
|
|
530
|
-
method.classList.remove('hidden');
|
|
531
|
-
} else {
|
|
532
|
-
method.classList.add('hidden');
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Hide test classes if all methods are hidden
|
|
537
|
-
testClasses.forEach(function(testClass) {
|
|
538
|
-
const visibleMethods = testClass.querySelectorAll('.test-method:not(.hidden)');
|
|
539
|
-
if (visibleMethods.length === 0) {
|
|
540
|
-
testClass.classList.add('hidden');
|
|
541
|
-
} else {
|
|
542
|
-
testClass.classList.remove('hidden');
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
JS
|
|
221
|
+
File.read(File.join(__dir__, 'assets', 'dashboard.js'))
|
|
548
222
|
end
|
|
549
223
|
end
|
|
550
224
|
end
|
|
@@ -35,30 +35,32 @@
|
|
|
35
35
|
<div class="test-results">
|
|
36
36
|
<% processed_tests.each do |test_class| %>
|
|
37
37
|
<div class="test-class">
|
|
38
|
-
<h2><%= test_class[:class_name] %></h2>
|
|
38
|
+
<h2><%= h(test_class[:class_name]) %></h2>
|
|
39
39
|
|
|
40
40
|
<% test_class[:methods].each do |method| %>
|
|
41
|
-
<div class="test-method" data-status="<%= method[:status] %>" data-name="<%= method[:name].downcase %>">
|
|
41
|
+
<div class="test-method" data-status="<%= h(method[:status]) %>" data-name="<%= h(method[:name].downcase) %>">
|
|
42
42
|
<div class="test-method-header">
|
|
43
|
-
<button class="expand-toggle" onclick="toggleTestMethod('<%= method[:safe_id] %>')">
|
|
44
|
-
<span class="expand-icon"
|
|
43
|
+
<button class="expand-toggle" onclick="toggleTestMethod('<%= h(method[:safe_id]) %>')">
|
|
44
|
+
<span class="expand-icon">▶</span>
|
|
45
45
|
</button>
|
|
46
|
-
<h3><%= method[:name] %></h3>
|
|
47
|
-
|
|
46
|
+
<h3><%= h(method[:name]) %></h3>
|
|
47
|
+
<% if method[:status] %>
|
|
48
|
+
<span class="method-status method-status-<%= h(method[:status]) %>"><%= h(method[:status]) %></span>
|
|
49
|
+
<% end %>
|
|
48
50
|
</div>
|
|
49
|
-
<div class="steps collapsed" id="steps-<%= method[:safe_id] %>">
|
|
51
|
+
<div class="steps collapsed" id="steps-<%= h(method[:safe_id]) %>">
|
|
50
52
|
<% method[:steps].each do |step| %>
|
|
51
|
-
<div class="step <%= step[:status] %>">
|
|
53
|
+
<div class="step <%= h(step[:status]) %>">
|
|
52
54
|
<div class="step-header">
|
|
53
|
-
<span class="step-name"><%= step[:name] %></span>
|
|
54
|
-
<span class="step-status"><%= step[:status] %></span>
|
|
55
|
+
<span class="step-name"><%= h(step[:name]) %></span>
|
|
56
|
+
<span class="step-status"><%= h(step[:status]) %></span>
|
|
55
57
|
</div>
|
|
56
|
-
<div class="step-detail"><%= step[:detail] %></div>
|
|
58
|
+
<div class="step-detail"><%= h(step[:detail]) %></div>
|
|
57
59
|
|
|
58
60
|
<% if step[:error] %>
|
|
59
61
|
<div class="error-log">
|
|
60
62
|
<h4>Error Details</h4>
|
|
61
|
-
<pre><%= step[:error] %></pre>
|
|
63
|
+
<pre><%= h(step[:error]) %></pre>
|
|
62
64
|
</div>
|
|
63
65
|
<% end %>
|
|
64
66
|
</div>
|
data/lib/capydash/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Damon Clark
|
|
@@ -62,6 +62,8 @@ files:
|
|
|
62
62
|
- README.md
|
|
63
63
|
- capydash.gemspec
|
|
64
64
|
- lib/capydash.rb
|
|
65
|
+
- lib/capydash/assets/dashboard.css
|
|
66
|
+
- lib/capydash/assets/dashboard.js
|
|
65
67
|
- lib/capydash/rspec.rb
|
|
66
68
|
- lib/capydash/templates/report.html.erb
|
|
67
69
|
- lib/capydash/version.rb
|