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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8120c468fac1e4d0e67b3190f2fd3905bf047a1628f66232cedbc3085e0af2f5
4
- data.tar.gz: ea62c0dd462b95ad7a2405f0f139b9c9f3d5f3bfa0c9315b93e70ee39df094e0
3
+ metadata.gz: dc3c0c3e8b55e78af53e8f1a8a34f44165da41b6149dec134cd2a573ba959a98
4
+ data.tar.gz: 4e3dbca12dc81a9914e418092def3c7a58a0bb1dfcad61d9b8d8056c99b2c063
5
5
  SHA512:
6
- metadata.gz: 0b2cbd4509f3dc292b2dc3873ae74e03fa3315cf850cd9bc10dbddfe63926c4cd03f1003fa728b527a3f48906149375709d432b347b774366607114209ec58a8
7
- data.tar.gz: ec5f1fb549172b312df50562c60c3e40a641518acd2f2e86e8288882a6c17820d4550cfe5824188ed50ea59567758a7af085010156ebec8b5806fb117e46ac74
6
+ metadata.gz: 578279a43a188c65e710d163fb30936f96ceee8d4a4803408a25683d6547695c48250f960d87f63179206b3e1345fe52290f40b184805a9b8c3e004205c05f09
7
+ data.tar.gz: 72218c84707dfd0e7b7275b87287862e45cf2bfa629169cea372be719a6314b63350a427831ad5016478fe443d3c680385b5cc5f879948e11048cdf494ba1f3f
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.
@@ -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
+ });
@@ -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
- file_path = example.metadata[:file_path] || ''
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(file_path)
145
- return 'UnknownSpec' if file_path.nil? || file_path.empty?
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
- filename = File.basename(file_path, '.rb')
148
- filename.split('_').map(&:capitalize).join('')
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
- erb.result(binding)
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
- <<~CSS
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
- <<~JS
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">▶</span>
43
+ <button class="expand-toggle" onclick="toggleTestMethod('<%= h(method[:safe_id]) %>')">
44
+ <span class="expand-icon">&#9654;</span>
45
45
  </button>
46
- <h3><%= method[:name] %></h3>
47
- <span class="method-status method-status-<%= method[:status] %>"><%= method[:status] %></span>
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>
@@ -1,3 +1,3 @@
1
1
  module CapyDash
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.5"
3
3
  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.3
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