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 +4 -4
- data/README.md +13 -110
- data/capydash.gemspec +3 -6
- data/lib/capydash/assets/dashboard.css +340 -0
- data/lib/capydash/assets/dashboard.js +94 -0
- data/lib/capydash/minitest.rb +72 -0
- data/lib/capydash/reporter.rb +180 -0
- data/lib/capydash/rspec.rb +50 -507
- data/lib/capydash/templates/report.html.erb +22 -12
- data/lib/capydash/version.rb +1 -1
- data/lib/capydash.rb +12 -6
- data/lib/minitest/capydash_plugin.rb +6 -0
- metadata +8 -17
data/lib/capydash/rspec.rb
CHANGED
|
@@ -1,94 +1,10 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require 'fileutils'
|
|
3
|
-
require 'erb'
|
|
1
|
+
require 'capydash/reporter'
|
|
4
2
|
|
|
5
3
|
module CapyDash
|
|
6
4
|
module RSpec
|
|
7
|
-
|
|
8
|
-
# Public method: Called from RSpec before(:suite) hook
|
|
9
|
-
def start_run
|
|
10
|
-
@results = []
|
|
11
|
-
@started_at = Time.now
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Public method: Called from RSpec after(:each) hook
|
|
15
|
-
def record_example(example)
|
|
16
|
-
return unless @started_at
|
|
17
|
-
|
|
18
|
-
execution_result = example.execution_result
|
|
19
|
-
status = normalize_status(execution_result.status)
|
|
20
|
-
|
|
21
|
-
error_message = nil
|
|
22
|
-
if execution_result.status == :failed && execution_result.exception
|
|
23
|
-
error_message = format_exception(execution_result.exception)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
file_path = example.metadata[:file_path] || ''
|
|
27
|
-
class_name = extract_class_name(file_path)
|
|
28
|
-
|
|
29
|
-
@results << {
|
|
30
|
-
class_name: class_name,
|
|
31
|
-
method_name: example.full_description,
|
|
32
|
-
status: status,
|
|
33
|
-
error: error_message,
|
|
34
|
-
location: example.metadata[:location]
|
|
35
|
-
}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Public method: Called from RSpec after(:suite) hook
|
|
39
|
-
def generate_report
|
|
40
|
-
return unless @started_at
|
|
41
|
-
return if @results.empty?
|
|
42
|
-
|
|
43
|
-
report_dir = File.join(Dir.pwd, "capydash_report")
|
|
44
|
-
FileUtils.mkdir_p(report_dir)
|
|
45
|
-
|
|
46
|
-
assets_dir = File.join(report_dir, "assets")
|
|
47
|
-
FileUtils.mkdir_p(assets_dir)
|
|
48
|
-
|
|
49
|
-
# Group results by class
|
|
50
|
-
tests_by_class = @results.group_by { |r| r[:class_name] }
|
|
51
|
-
|
|
52
|
-
# Calculate statistics
|
|
53
|
-
total_tests = @results.length
|
|
54
|
-
passed_tests = @results.count { |r| r[:status] == 'passed' }
|
|
55
|
-
failed_tests = @results.count { |r| r[:status] == 'failed' }
|
|
56
|
-
|
|
57
|
-
# Process for template
|
|
58
|
-
processed_tests = tests_by_class.map do |class_name, examples|
|
|
59
|
-
{
|
|
60
|
-
class_name: class_name,
|
|
61
|
-
methods: examples.map do |ex|
|
|
62
|
-
{
|
|
63
|
-
name: ex[:method_name],
|
|
64
|
-
status: ex[:status],
|
|
65
|
-
steps: [{
|
|
66
|
-
name: 'test_execution',
|
|
67
|
-
detail: ex[:method_name],
|
|
68
|
-
status: ex[:status],
|
|
69
|
-
error: ex[:error]
|
|
70
|
-
}]
|
|
71
|
-
}
|
|
72
|
-
end
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Generate HTML
|
|
77
|
-
html_content = generate_html(processed_tests, @started_at, total_tests, passed_tests, failed_tests)
|
|
78
|
-
File.write(File.join(report_dir, "index.html"), html_content)
|
|
79
|
-
|
|
80
|
-
# Generate CSS
|
|
81
|
-
css_content = generate_css
|
|
82
|
-
File.write(File.join(assets_dir, "dashboard.css"), css_content)
|
|
83
|
-
|
|
84
|
-
# Generate JS
|
|
85
|
-
js_content = generate_javascript
|
|
86
|
-
File.write(File.join(assets_dir, "dashboard.js"), js_content)
|
|
87
|
-
|
|
88
|
-
report_dir
|
|
89
|
-
end
|
|
5
|
+
extend CapyDash::Reporter
|
|
90
6
|
|
|
91
|
-
|
|
7
|
+
class << self
|
|
92
8
|
def setup!
|
|
93
9
|
return unless rspec_available?
|
|
94
10
|
return if @configured
|
|
@@ -112,444 +28,71 @@ module CapyDash
|
|
|
112
28
|
end
|
|
113
29
|
end
|
|
114
30
|
rescue => e
|
|
115
|
-
# If RSpec isn't ready, silently fail - it will be set up later
|
|
116
31
|
@configured = false
|
|
117
32
|
end
|
|
118
33
|
end
|
|
119
34
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def rspec_available?
|
|
123
|
-
return false unless defined?(::RSpec)
|
|
124
|
-
return false unless ::RSpec.respond_to?(:configure)
|
|
125
|
-
true
|
|
126
|
-
rescue
|
|
127
|
-
false
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def normalize_status(status)
|
|
131
|
-
# Normalize RSpec status symbols to strings
|
|
132
|
-
case status
|
|
133
|
-
when :passed, 'passed'
|
|
134
|
-
'passed'
|
|
135
|
-
when :failed, 'failed'
|
|
136
|
-
'failed'
|
|
137
|
-
when :pending, 'pending'
|
|
138
|
-
'pending'
|
|
139
|
-
else
|
|
140
|
-
status.to_s
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def extract_class_name(file_path)
|
|
145
|
-
return 'UnknownSpec' if file_path.nil? || file_path.empty?
|
|
146
|
-
|
|
147
|
-
filename = File.basename(file_path, '.rb')
|
|
148
|
-
filename.split('_').map(&:capitalize).join('')
|
|
149
|
-
end
|
|
35
|
+
def record_example(example)
|
|
36
|
+
return unless @started_at
|
|
150
37
|
|
|
151
|
-
|
|
152
|
-
return nil unless exception
|
|
38
|
+
execution_result = example.execution_result
|
|
153
39
|
|
|
154
|
-
|
|
155
|
-
|
|
40
|
+
# Derive status from exception since execution_result.status
|
|
41
|
+
# is not yet set during after(:each) hooks
|
|
42
|
+
status = if example.pending? || example.skipped?
|
|
43
|
+
'pending'
|
|
44
|
+
elsif execution_result.exception
|
|
45
|
+
'failed'
|
|
46
|
+
else
|
|
47
|
+
'passed'
|
|
48
|
+
end
|
|
156
49
|
|
|
157
|
-
|
|
158
|
-
if
|
|
159
|
-
|
|
50
|
+
error_message = nil
|
|
51
|
+
if execution_result.exception
|
|
52
|
+
error_message = format_exception(execution_result.exception)
|
|
160
53
|
end
|
|
161
54
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def generate_html(processed_tests, created_at, total_tests, passed_tests, failed_tests)
|
|
166
|
-
# Create safe IDs for method names (escape special chars for HTML/JS)
|
|
167
|
-
processed_tests.each do |test_class|
|
|
168
|
-
test_class[:methods].each do |method|
|
|
169
|
-
method[:safe_id] = method[:name].gsub(/['"]/, '').gsub(/[^a-zA-Z0-9]/, '_')
|
|
170
|
-
end
|
|
55
|
+
screenshot_path = nil
|
|
56
|
+
if status == 'failed'
|
|
57
|
+
screenshot_path = capture_screenshot
|
|
171
58
|
end
|
|
172
59
|
|
|
173
|
-
|
|
174
|
-
template = File.read(template_path)
|
|
175
|
-
erb = ERB.new(template)
|
|
60
|
+
class_name = extract_class_name(example)
|
|
176
61
|
|
|
177
|
-
|
|
62
|
+
record_result({
|
|
63
|
+
class_name: class_name,
|
|
64
|
+
method_name: example.full_description,
|
|
65
|
+
status: status,
|
|
66
|
+
error: error_message,
|
|
67
|
+
location: example.metadata[:location],
|
|
68
|
+
screenshot_path: screenshot_path
|
|
69
|
+
})
|
|
178
70
|
end
|
|
179
71
|
|
|
180
|
-
|
|
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
|
-
display: inline-flex;
|
|
338
|
-
align-items: center;
|
|
339
|
-
padding: 0.35rem 0.85rem;
|
|
340
|
-
border-radius: 4px;
|
|
341
|
-
font-size: 0.75rem;
|
|
342
|
-
font-weight: 600;
|
|
343
|
-
text-transform: uppercase;
|
|
344
|
-
letter-spacing: 0.5px;
|
|
345
|
-
white-space: nowrap;
|
|
346
|
-
min-width: 60px;
|
|
347
|
-
justify-content: center;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.method-status-passed {
|
|
351
|
-
background-color: #27ae60;
|
|
352
|
-
color: white;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.method-status-failed {
|
|
356
|
-
background-color: #e74c3c;
|
|
357
|
-
color: white;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
.method-status-pending {
|
|
361
|
-
background-color: #f39c12;
|
|
362
|
-
color: white;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
.expand-toggle {
|
|
366
|
-
background: none;
|
|
367
|
-
border: none;
|
|
368
|
-
cursor: pointer;
|
|
369
|
-
padding: 0.5rem;
|
|
370
|
-
border-radius: 4px;
|
|
371
|
-
transition: background-color 0.2s;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
.expand-toggle:hover {
|
|
375
|
-
background-color: #f0f0f0;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
.expand-icon {
|
|
379
|
-
font-size: 0.8rem;
|
|
380
|
-
color: #666;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
.steps {
|
|
384
|
-
display: flex;
|
|
385
|
-
flex-direction: column;
|
|
386
|
-
gap: 1rem;
|
|
387
|
-
transition: max-height 0.3s ease-out;
|
|
388
|
-
overflow: hidden;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.steps.collapsed {
|
|
392
|
-
max-height: 0;
|
|
393
|
-
margin: 0;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
.step {
|
|
397
|
-
border: 1px solid #ddd;
|
|
398
|
-
border-radius: 6px;
|
|
399
|
-
padding: 1rem;
|
|
400
|
-
background: #fafafa;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
.step.passed {
|
|
404
|
-
border-color: #27ae60;
|
|
405
|
-
background: #f8fff8;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.step.failed {
|
|
409
|
-
border-color: #e74c3c;
|
|
410
|
-
background: #fff8f8;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
.step-header {
|
|
414
|
-
display: flex;
|
|
415
|
-
justify-content: space-between;
|
|
416
|
-
align-items: center;
|
|
417
|
-
margin-bottom: 0.5rem;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
.step-name {
|
|
421
|
-
font-weight: 600;
|
|
422
|
-
color: #2c3e50;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.step-status {
|
|
426
|
-
padding: 0.25rem 0.5rem;
|
|
427
|
-
border-radius: 4px;
|
|
428
|
-
font-size: 0.8rem;
|
|
429
|
-
font-weight: 600;
|
|
430
|
-
text-transform: uppercase;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.step.passed .step-status {
|
|
434
|
-
background: #27ae60;
|
|
435
|
-
color: white;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
.step.failed .step-status {
|
|
439
|
-
background: #e74c3c;
|
|
440
|
-
color: white;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
.step-detail {
|
|
444
|
-
color: #666;
|
|
445
|
-
font-size: 0.9rem;
|
|
446
|
-
margin-bottom: 0.5rem;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
.error-log {
|
|
450
|
-
margin-top: 1rem;
|
|
451
|
-
padding: 1rem;
|
|
452
|
-
background: #fff5f5;
|
|
453
|
-
border: 1px solid #fed7d7;
|
|
454
|
-
border-radius: 6px;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.error-log h4 {
|
|
458
|
-
color: #e53e3e;
|
|
459
|
-
margin: 0 0 0.5rem 0;
|
|
460
|
-
font-size: 0.9rem;
|
|
461
|
-
}
|
|
72
|
+
private
|
|
462
73
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
font-size: 0.8rem;
|
|
470
|
-
line-height: 1.4;
|
|
471
|
-
margin: 0;
|
|
472
|
-
white-space: pre-wrap;
|
|
473
|
-
word-wrap: break-word;
|
|
474
|
-
}
|
|
475
|
-
CSS
|
|
74
|
+
def rspec_available?
|
|
75
|
+
return false unless defined?(::RSpec)
|
|
76
|
+
return false unless ::RSpec.respond_to?(:configure)
|
|
77
|
+
true
|
|
78
|
+
rescue
|
|
79
|
+
false
|
|
476
80
|
end
|
|
477
81
|
|
|
478
|
-
def
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (stepsContainer && button) {
|
|
485
|
-
const icon = button.querySelector('.expand-icon');
|
|
486
|
-
if (icon) {
|
|
487
|
-
const isCollapsed = stepsContainer.classList.contains('collapsed');
|
|
488
|
-
|
|
489
|
-
if (isCollapsed) {
|
|
490
|
-
stepsContainer.classList.remove('collapsed');
|
|
491
|
-
icon.textContent = '▼';
|
|
492
|
-
} else {
|
|
493
|
-
stepsContainer.classList.add('collapsed');
|
|
494
|
-
icon.textContent = '▶';
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Search functionality
|
|
501
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
502
|
-
const searchInput = document.getElementById('searchInput');
|
|
503
|
-
if (!searchInput) return;
|
|
504
|
-
|
|
505
|
-
searchInput.addEventListener('input', function(e) {
|
|
506
|
-
const query = e.target.value.toLowerCase().trim();
|
|
507
|
-
const testMethods = document.querySelectorAll('.test-method');
|
|
508
|
-
const testClasses = document.querySelectorAll('.test-class');
|
|
509
|
-
|
|
510
|
-
// Determine if query is a status filter
|
|
511
|
-
const isStatusFilter = query === 'pass' || query === 'fail' ||
|
|
512
|
-
query === 'passed' || query === 'failed' ||
|
|
513
|
-
query === 'pending';
|
|
514
|
-
|
|
515
|
-
testMethods.forEach(function(method) {
|
|
516
|
-
const name = method.getAttribute('data-name') || '';
|
|
517
|
-
const status = method.getAttribute('data-status') || '';
|
|
518
|
-
|
|
519
|
-
let shouldShow = false;
|
|
520
|
-
|
|
521
|
-
if (!query) {
|
|
522
|
-
// No query - show all
|
|
523
|
-
shouldShow = true;
|
|
524
|
-
} else if (isStatusFilter) {
|
|
525
|
-
// Status filter - check status
|
|
526
|
-
shouldShow = (query === 'pass' && status === 'passed') ||
|
|
527
|
-
(query === 'fail' && status === 'failed') ||
|
|
528
|
-
query === status;
|
|
529
|
-
} else {
|
|
530
|
-
// Name filter - check if name contains query
|
|
531
|
-
shouldShow = name.includes(query);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (shouldShow) {
|
|
535
|
-
method.classList.remove('hidden');
|
|
536
|
-
} else {
|
|
537
|
-
method.classList.add('hidden');
|
|
538
|
-
}
|
|
539
|
-
});
|
|
82
|
+
def extract_class_name(example)
|
|
83
|
+
group = example.metadata[:example_group]
|
|
84
|
+
while group && group[:parent_example_group]
|
|
85
|
+
group = group[:parent_example_group]
|
|
86
|
+
end
|
|
540
87
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
|
-
JS
|
|
88
|
+
if group && group[:description] && !group[:description].to_s.empty?
|
|
89
|
+
group[:description].to_s
|
|
90
|
+
else
|
|
91
|
+
file_path = example.metadata[:file_path] || ''
|
|
92
|
+
return 'UnknownSpec' if file_path.empty?
|
|
93
|
+
filename = File.basename(file_path, '.rb')
|
|
94
|
+
filename.split('_').map(&:capitalize).join('')
|
|
95
|
+
end
|
|
553
96
|
end
|
|
554
97
|
end
|
|
555
98
|
end
|
|
@@ -35,32 +35,39 @@
|
|
|
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>
|
|
46
|
+
<h3><%= h(method[:name]) %></h3>
|
|
47
47
|
<% if method[:status] %>
|
|
48
|
-
<span class="method-status method-status-<%= method[:status] %>"><%= method[:status] %></span>
|
|
48
|
+
<span class="method-status method-status-<%= h(method[:status]) %>"><%= h(method[:status]) %></span>
|
|
49
49
|
<% end %>
|
|
50
50
|
</div>
|
|
51
|
-
<div class="steps collapsed" id="steps-<%= method[:safe_id] %>">
|
|
51
|
+
<div class="steps collapsed" id="steps-<%= h(method[:safe_id]) %>">
|
|
52
52
|
<% method[:steps].each do |step| %>
|
|
53
|
-
<div class="step <%= step[:status] %>">
|
|
53
|
+
<div class="step <%= h(step[:status]) %>">
|
|
54
54
|
<div class="step-header">
|
|
55
|
-
<span class="step-name"><%= step[:name] %></span>
|
|
56
|
-
<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>
|
|
57
57
|
</div>
|
|
58
|
-
<div class="step-detail"><%= step[:detail] %></div>
|
|
58
|
+
<div class="step-detail"><%= h(step[:detail]) %></div>
|
|
59
59
|
|
|
60
60
|
<% if step[:error] %>
|
|
61
61
|
<div class="error-log">
|
|
62
62
|
<h4>Error Details</h4>
|
|
63
|
-
<pre><%= step[:error] %></pre>
|
|
63
|
+
<pre><%= h(step[:error]) %></pre>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<% if step[:screenshot] %>
|
|
68
|
+
<div class="screenshot-container" onclick="openLightbox('<%= h(step[:screenshot]) %>')">
|
|
69
|
+
<h4>Failure Screenshot</h4>
|
|
70
|
+
<img src="<%= h(step[:screenshot]) %>" alt="Failure screenshot">
|
|
64
71
|
</div>
|
|
65
72
|
<% end %>
|
|
66
73
|
</div>
|
|
@@ -72,6 +79,9 @@
|
|
|
72
79
|
<% end %>
|
|
73
80
|
</div>
|
|
74
81
|
</div>
|
|
82
|
+
<div class="lightbox" id="screenshotLightbox" onclick="closeLightbox()">
|
|
83
|
+
<img id="lightboxImage" src="" alt="Screenshot full view">
|
|
84
|
+
</div>
|
|
75
85
|
<script src="assets/dashboard.js"></script>
|
|
76
86
|
</body>
|
|
77
87
|
</html>
|
data/lib/capydash/version.rb
CHANGED
data/lib/capydash.rb
CHANGED
|
@@ -4,12 +4,18 @@ require "capydash/version"
|
|
|
4
4
|
if defined?(RSpec) && RSpec.respond_to?(:configure)
|
|
5
5
|
require "capydash/rspec"
|
|
6
6
|
CapyDash::RSpec.setup!
|
|
7
|
-
elsif defined?(Rails)
|
|
8
|
-
# In Rails, RSpec might load after the gem, so
|
|
9
|
-
Rails
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
elsif defined?(Rails) && defined?(Rails::Railtie)
|
|
8
|
+
# In Rails, RSpec might load after the gem, so defer setup
|
|
9
|
+
class CapyDash::Railtie < Rails::Railtie
|
|
10
|
+
config.after_initialize do
|
|
11
|
+
if defined?(RSpec) && RSpec.respond_to?(:configure)
|
|
12
|
+
require "capydash/rspec" unless defined?(CapyDash::RSpec)
|
|
13
|
+
CapyDash::RSpec.setup!
|
|
14
|
+
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
end
|
|
18
|
+
|
|
19
|
+
# Minitest integration is handled automatically via the minitest plugin system.
|
|
20
|
+
# Minitest discovers lib/minitest/capydash_plugin.rb and calls
|
|
21
|
+
# plugin_capydash_init during Minitest.run, which adds our reporter.
|