capydash 0.2.5 → 0.3.1
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 +23 -4
- data/capydash.gemspec +3 -6
- data/lib/capydash/assets/dashboard.css +47 -0
- data/lib/capydash/assets/dashboard.js +22 -0
- data/lib/capydash/minitest.rb +79 -0
- data/lib/capydash/reporter.rb +180 -0
- data/lib/capydash/rspec.rb +41 -166
- data/lib/capydash/templates/report.html.erb +10 -0
- data/lib/capydash/version.rb +1 -1
- data/lib/capydash.rb +12 -6
- data/lib/minitest/capydash_plugin.rb +6 -0
- metadata +6 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59bf6aaa616cf19064f4fc9d98b53f81fc79f54ef604c2034636f6ea8dbf936d
|
|
4
|
+
data.tar.gz: 605412b29c494e55a96eb8a79293e2bd4b68cf06f3c6fbb0f0be7efbf1e362d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 496f1563f50549166ae3ff4a0e31fce18941522401bc9aa0a83076907a9996238d9102896857aabf8ecf813a92a89b9daeb9d25753ff097ea966cd5b4b2509a4
|
|
7
|
+
data.tar.gz: c070c748dbbe64238712368702279a49bb2a5c0c6af4635916b5e39308b8d81ddf92dda24b9369517c52fa48a48937b810b0901044b6c98820eac062d002fcb5
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CapyDash
|
|
2
2
|
|
|
3
|
-
Minimal, zero-config HTML report for your RSpec tests. Add the gem, run your tests, get a report.
|
|
3
|
+
Minimal, zero-config HTML report for your RSpec and Minitest tests. Add the gem, run your tests, get a report.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -21,7 +21,12 @@ Run `bundle install`. That's it — no configuration needed.
|
|
|
21
21
|
Run your tests as usual:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
+
# RSpec
|
|
24
25
|
bundle exec rspec
|
|
26
|
+
|
|
27
|
+
# Minitest
|
|
28
|
+
bundle exec rails test
|
|
29
|
+
bundle exec rails test:system
|
|
25
30
|
```
|
|
26
31
|
|
|
27
32
|
After the suite finishes, open the generated report:
|
|
@@ -30,16 +35,30 @@ After the suite finishes, open the generated report:
|
|
|
30
35
|
capydash_report/index.html
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
The report includes pass/fail counts, tests grouped by
|
|
38
|
+
The report includes pass/fail counts, tests grouped by class, expandable error details with backtraces, and failure screenshots with a clickable lightbox.
|
|
39
|
+
|
|
40
|
+
## Failure Screenshots
|
|
41
|
+
|
|
42
|
+
When a test fails and Capybara with a browser driver is available, CapyDash automatically captures a screenshot and embeds it in the report. Click the thumbnail to view the full-size image.
|
|
43
|
+
|
|
44
|
+
- **RSpec** — screenshot captured during `after(:each)`, before session teardown
|
|
45
|
+
- **Minitest** — uses Rails' built-in failure screenshot from `tmp/capybara/`
|
|
46
|
+
|
|
47
|
+
No configuration needed. If Capybara isn't available, screenshots are silently skipped.
|
|
34
48
|
|
|
35
49
|
## Requirements
|
|
36
50
|
|
|
37
|
-
- RSpec >= 3.0
|
|
51
|
+
- RSpec >= 3.0 **or** Minitest >= 5.0
|
|
38
52
|
- Ruby 2.7+
|
|
39
53
|
|
|
40
54
|
## How It Works
|
|
41
55
|
|
|
42
|
-
CapyDash
|
|
56
|
+
CapyDash auto-detects your test framework and hooks in automatically:
|
|
57
|
+
|
|
58
|
+
- **RSpec** — registers `before(:suite)`, `after(:each)`, and `after(:suite)` callbacks via `RSpec.configure`
|
|
59
|
+
- **Minitest** — registers a reporter via the [Minitest plugin system](https://docs.seattlerb.org/minitest/Minitest.html) (`start`, `record`, `report`)
|
|
60
|
+
|
|
61
|
+
Results are collected in memory during the run and written as a static HTML report to `capydash_report/` when the suite completes. Each run produces a fresh report — no server, no database, no config files.
|
|
43
62
|
|
|
44
63
|
## License
|
|
45
64
|
|
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
|
-
#
|
|
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
|
|
|
@@ -291,3 +291,50 @@ body {
|
|
|
291
291
|
white-space: pre-wrap;
|
|
292
292
|
word-wrap: break-word;
|
|
293
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
|
+
}
|
|
@@ -1,3 +1,25 @@
|
|
|
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
|
+
|
|
1
23
|
function toggleTestMethod(safeId) {
|
|
2
24
|
const stepsContainer = document.getElementById('steps-' + safeId);
|
|
3
25
|
const button = document.querySelector('[onclick*="' + safeId + '"]');
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
return unless system_test?(result)
|
|
15
|
+
|
|
16
|
+
status = if result.skipped?
|
|
17
|
+
'pending'
|
|
18
|
+
elsif result.passed?
|
|
19
|
+
'passed'
|
|
20
|
+
else
|
|
21
|
+
'failed'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
error_message = nil
|
|
25
|
+
if result.failure
|
|
26
|
+
error_message = format_exception(result.failure)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
screenshot_path = nil
|
|
30
|
+
if status == 'failed'
|
|
31
|
+
# Rails system tests save screenshots before teardown to tmp/capybara/.
|
|
32
|
+
# By the time the reporter's record() runs, the session is torn down,
|
|
33
|
+
# so we look for the Rails-generated screenshot first.
|
|
34
|
+
screenshot_path = find_rails_screenshot(result.name) || capture_screenshot
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class_name = result.klass || 'UnknownTest'
|
|
38
|
+
method_name = result.name.to_s.sub(/\Atest_/, '').tr('_', ' ')
|
|
39
|
+
|
|
40
|
+
location = nil
|
|
41
|
+
if result.respond_to?(:source_location) && result.source_location
|
|
42
|
+
location = result.source_location.join(':')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
record_result({
|
|
46
|
+
class_name: class_name,
|
|
47
|
+
method_name: method_name,
|
|
48
|
+
status: status,
|
|
49
|
+
error: error_message,
|
|
50
|
+
location: location,
|
|
51
|
+
screenshot_path: screenshot_path
|
|
52
|
+
})
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def report
|
|
56
|
+
generate_report
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def passed?
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def system_test?(result)
|
|
66
|
+
return false unless defined?(::ActionDispatch::SystemTestCase)
|
|
67
|
+
klass = Object.const_get(result.klass) rescue nil
|
|
68
|
+
klass && klass <= ::ActionDispatch::SystemTestCase
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def find_rails_screenshot(test_name)
|
|
72
|
+
path = File.join(Dir.pwd, "tmp", "capybara", "failures_#{test_name}.png")
|
|
73
|
+
File.exist?(path) ? path : nil
|
|
74
|
+
rescue
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
module CapyDash
|
|
8
|
+
class ReportData
|
|
9
|
+
attr_reader :processed_tests, :created_at, :total_tests, :passed_tests, :failed_tests
|
|
10
|
+
|
|
11
|
+
def initialize(processed_tests:, created_at:, total_tests:, passed_tests:, failed_tests:)
|
|
12
|
+
@processed_tests = processed_tests
|
|
13
|
+
@created_at = created_at
|
|
14
|
+
@total_tests = total_tests
|
|
15
|
+
@passed_tests = passed_tests
|
|
16
|
+
@failed_tests = failed_tests
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def h(text)
|
|
20
|
+
CGI.escapeHTML(text.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get_binding
|
|
24
|
+
binding
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module Reporter
|
|
29
|
+
def start_run
|
|
30
|
+
@results = []
|
|
31
|
+
@started_at = Time.now
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def record_result(result_hash)
|
|
35
|
+
return unless @started_at
|
|
36
|
+
@results << result_hash
|
|
37
|
+
end
|
|
38
|
+
|
|
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
|
+
screenshots_dir = File.join(assets_dir, "screenshots")
|
|
50
|
+
FileUtils.rm_rf(screenshots_dir)
|
|
51
|
+
FileUtils.mkdir_p(screenshots_dir)
|
|
52
|
+
|
|
53
|
+
# Group results by class
|
|
54
|
+
tests_by_class = @results.group_by { |r| r[:class_name] }
|
|
55
|
+
|
|
56
|
+
# Calculate statistics
|
|
57
|
+
total_tests = @results.length
|
|
58
|
+
passed_tests = @results.count { |r| r[:status] == 'passed' }
|
|
59
|
+
failed_tests = @results.count { |r| r[:status] == 'failed' }
|
|
60
|
+
|
|
61
|
+
# Copy screenshots into report and build relative paths
|
|
62
|
+
screenshot_index = 0
|
|
63
|
+
@results.each do |result|
|
|
64
|
+
if result[:screenshot_path] && File.exist?(result[:screenshot_path])
|
|
65
|
+
screenshot_index += 1
|
|
66
|
+
dest_name = format("%03d.png", screenshot_index)
|
|
67
|
+
dest_path = File.join(screenshots_dir, dest_name)
|
|
68
|
+
FileUtils.cp(result[:screenshot_path], dest_path)
|
|
69
|
+
result[:screenshot_relative] = "assets/screenshots/#{dest_name}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Process for template
|
|
74
|
+
processed_tests = tests_by_class.map do |class_name, examples|
|
|
75
|
+
{
|
|
76
|
+
class_name: class_name,
|
|
77
|
+
methods: examples.map do |ex|
|
|
78
|
+
{
|
|
79
|
+
name: ex[:method_name],
|
|
80
|
+
status: ex[:status],
|
|
81
|
+
steps: [{
|
|
82
|
+
name: 'test_execution',
|
|
83
|
+
detail: ex[:method_name],
|
|
84
|
+
status: ex[:status],
|
|
85
|
+
error: ex[:error],
|
|
86
|
+
screenshot: ex[:screenshot_relative]
|
|
87
|
+
}]
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Generate HTML
|
|
94
|
+
html_content = generate_html(processed_tests, @started_at, total_tests, passed_tests, failed_tests)
|
|
95
|
+
File.write(File.join(report_dir, "index.html"), html_content)
|
|
96
|
+
|
|
97
|
+
# Generate CSS
|
|
98
|
+
css_content = generate_css
|
|
99
|
+
File.write(File.join(assets_dir, "dashboard.css"), css_content)
|
|
100
|
+
|
|
101
|
+
# Generate JS
|
|
102
|
+
js_content = generate_javascript
|
|
103
|
+
File.write(File.join(assets_dir, "dashboard.js"), js_content)
|
|
104
|
+
|
|
105
|
+
report_dir
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def normalize_status(status)
|
|
109
|
+
case status
|
|
110
|
+
when :passed, 'passed'
|
|
111
|
+
'passed'
|
|
112
|
+
when :failed, 'failed'
|
|
113
|
+
'failed'
|
|
114
|
+
when :pending, 'pending', :skipped, 'skipped'
|
|
115
|
+
'pending'
|
|
116
|
+
else
|
|
117
|
+
status.to_s
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_exception(exception)
|
|
122
|
+
return nil unless exception
|
|
123
|
+
|
|
124
|
+
message = exception.message || 'Unknown error'
|
|
125
|
+
backtrace = exception.backtrace || []
|
|
126
|
+
|
|
127
|
+
formatted = "#{exception.class}: #{message}"
|
|
128
|
+
if backtrace.any?
|
|
129
|
+
formatted += "\n" + backtrace.first(5).map { |line| " #{line}" }.join("\n")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
formatted
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def capture_screenshot
|
|
136
|
+
return nil unless defined?(::Capybara) && defined?(::Capybara.current_session)
|
|
137
|
+
|
|
138
|
+
session = ::Capybara.current_session
|
|
139
|
+
return nil unless session.respond_to?(:save_screenshot)
|
|
140
|
+
|
|
141
|
+
tmpfile = File.join(Dir.tmpdir, "capydash_#{Time.now.to_i}_#{rand(10000)}.png")
|
|
142
|
+
session.save_screenshot(tmpfile)
|
|
143
|
+
tmpfile
|
|
144
|
+
rescue => _e
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def generate_html(processed_tests, created_at, total_tests, passed_tests, failed_tests)
|
|
151
|
+
processed_tests.each do |test_class|
|
|
152
|
+
test_class[:methods].each do |method|
|
|
153
|
+
method[:safe_id] = method[:name].gsub(/['"]/, '').gsub(/[^a-zA-Z0-9]/, '_')
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
template_path = File.join(File.dirname(__FILE__), 'templates', 'report.html.erb')
|
|
158
|
+
template = File.read(template_path)
|
|
159
|
+
erb = ERB.new(template)
|
|
160
|
+
|
|
161
|
+
report_data = CapyDash::ReportData.new(
|
|
162
|
+
processed_tests: processed_tests,
|
|
163
|
+
created_at: created_at,
|
|
164
|
+
total_tests: total_tests,
|
|
165
|
+
passed_tests: passed_tests,
|
|
166
|
+
failed_tests: failed_tests
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
erb.result(report_data.get_binding)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def generate_css
|
|
173
|
+
File.read(File.join(File.dirname(__FILE__), 'assets', 'dashboard.css'))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def generate_javascript
|
|
177
|
+
File.read(File.join(File.dirname(__FILE__), 'assets', 'dashboard.js'))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
data/lib/capydash/rspec.rb
CHANGED
|
@@ -1,114 +1,10 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require 'fileutils'
|
|
3
|
-
require 'erb'
|
|
4
|
-
require 'cgi'
|
|
1
|
+
require 'capydash/reporter'
|
|
5
2
|
|
|
6
3
|
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
|
-
|
|
27
4
|
module RSpec
|
|
28
|
-
|
|
29
|
-
# Public method: Called from RSpec before(:suite) hook
|
|
30
|
-
def start_run
|
|
31
|
-
@results = []
|
|
32
|
-
@started_at = Time.now
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Public method: Called from RSpec after(:each) hook
|
|
36
|
-
def record_example(example)
|
|
37
|
-
return unless @started_at
|
|
38
|
-
|
|
39
|
-
execution_result = example.execution_result
|
|
40
|
-
status = normalize_status(execution_result.status)
|
|
41
|
-
|
|
42
|
-
error_message = nil
|
|
43
|
-
if execution_result.status == :failed && execution_result.exception
|
|
44
|
-
error_message = format_exception(execution_result.exception)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
class_name = extract_class_name(example)
|
|
5
|
+
extend CapyDash::Reporter
|
|
48
6
|
|
|
49
|
-
|
|
50
|
-
class_name: class_name,
|
|
51
|
-
method_name: example.full_description,
|
|
52
|
-
status: status,
|
|
53
|
-
error: error_message,
|
|
54
|
-
location: example.metadata[:location]
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Public method: Called from RSpec after(:suite) hook
|
|
59
|
-
def generate_report
|
|
60
|
-
return unless @started_at
|
|
61
|
-
return if @results.empty?
|
|
62
|
-
|
|
63
|
-
report_dir = File.join(Dir.pwd, "capydash_report")
|
|
64
|
-
FileUtils.mkdir_p(report_dir)
|
|
65
|
-
|
|
66
|
-
assets_dir = File.join(report_dir, "assets")
|
|
67
|
-
FileUtils.mkdir_p(assets_dir)
|
|
68
|
-
|
|
69
|
-
# Group results by class
|
|
70
|
-
tests_by_class = @results.group_by { |r| r[:class_name] }
|
|
71
|
-
|
|
72
|
-
# Calculate statistics
|
|
73
|
-
total_tests = @results.length
|
|
74
|
-
passed_tests = @results.count { |r| r[:status] == 'passed' }
|
|
75
|
-
failed_tests = @results.count { |r| r[:status] == 'failed' }
|
|
76
|
-
|
|
77
|
-
# Process for template
|
|
78
|
-
processed_tests = tests_by_class.map do |class_name, examples|
|
|
79
|
-
{
|
|
80
|
-
class_name: class_name,
|
|
81
|
-
methods: examples.map do |ex|
|
|
82
|
-
{
|
|
83
|
-
name: ex[:method_name],
|
|
84
|
-
status: ex[:status],
|
|
85
|
-
steps: [{
|
|
86
|
-
name: 'test_execution',
|
|
87
|
-
detail: ex[:method_name],
|
|
88
|
-
status: ex[:status],
|
|
89
|
-
error: ex[:error]
|
|
90
|
-
}]
|
|
91
|
-
}
|
|
92
|
-
end
|
|
93
|
-
}
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Generate HTML
|
|
97
|
-
html_content = generate_html(processed_tests, @started_at, total_tests, passed_tests, failed_tests)
|
|
98
|
-
File.write(File.join(report_dir, "index.html"), html_content)
|
|
99
|
-
|
|
100
|
-
# Generate CSS
|
|
101
|
-
css_content = generate_css
|
|
102
|
-
File.write(File.join(assets_dir, "dashboard.css"), css_content)
|
|
103
|
-
|
|
104
|
-
# Generate JS
|
|
105
|
-
js_content = generate_javascript
|
|
106
|
-
File.write(File.join(assets_dir, "dashboard.js"), js_content)
|
|
107
|
-
|
|
108
|
-
report_dir
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Public method: Sets up RSpec hooks
|
|
7
|
+
class << self
|
|
112
8
|
def setup!
|
|
113
9
|
return unless rspec_available?
|
|
114
10
|
return if @configured
|
|
@@ -132,11 +28,48 @@ module CapyDash
|
|
|
132
28
|
end
|
|
133
29
|
end
|
|
134
30
|
rescue => e
|
|
135
|
-
# If RSpec isn't ready, silently fail - it will be set up later
|
|
136
31
|
@configured = false
|
|
137
32
|
end
|
|
138
33
|
end
|
|
139
34
|
|
|
35
|
+
def record_example(example)
|
|
36
|
+
return unless @started_at
|
|
37
|
+
return unless example.metadata[:type] == :system
|
|
38
|
+
|
|
39
|
+
execution_result = example.execution_result
|
|
40
|
+
|
|
41
|
+
# Derive status from exception since execution_result.status
|
|
42
|
+
# is not yet set during after(:each) hooks
|
|
43
|
+
status = if example.pending? || example.skipped?
|
|
44
|
+
'pending'
|
|
45
|
+
elsif execution_result.exception
|
|
46
|
+
'failed'
|
|
47
|
+
else
|
|
48
|
+
'passed'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
error_message = nil
|
|
52
|
+
if execution_result.exception
|
|
53
|
+
error_message = format_exception(execution_result.exception)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
screenshot_path = nil
|
|
57
|
+
if status == 'failed'
|
|
58
|
+
screenshot_path = capture_screenshot
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class_name = extract_class_name(example)
|
|
62
|
+
|
|
63
|
+
record_result({
|
|
64
|
+
class_name: class_name,
|
|
65
|
+
method_name: example.full_description,
|
|
66
|
+
status: status,
|
|
67
|
+
error: error_message,
|
|
68
|
+
location: example.metadata[:location],
|
|
69
|
+
screenshot_path: screenshot_path
|
|
70
|
+
})
|
|
71
|
+
end
|
|
72
|
+
|
|
140
73
|
private
|
|
141
74
|
|
|
142
75
|
def rspec_available?
|
|
@@ -147,19 +80,6 @@ module CapyDash
|
|
|
147
80
|
false
|
|
148
81
|
end
|
|
149
82
|
|
|
150
|
-
def normalize_status(status)
|
|
151
|
-
case status
|
|
152
|
-
when :passed, 'passed'
|
|
153
|
-
'passed'
|
|
154
|
-
when :failed, 'failed'
|
|
155
|
-
'failed'
|
|
156
|
-
when :pending, 'pending'
|
|
157
|
-
'pending'
|
|
158
|
-
else
|
|
159
|
-
status.to_s
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
83
|
def extract_class_name(example)
|
|
164
84
|
group = example.metadata[:example_group]
|
|
165
85
|
while group && group[:parent_example_group]
|
|
@@ -175,51 +95,6 @@ module CapyDash
|
|
|
175
95
|
filename.split('_').map(&:capitalize).join('')
|
|
176
96
|
end
|
|
177
97
|
end
|
|
178
|
-
|
|
179
|
-
def format_exception(exception)
|
|
180
|
-
return nil unless exception
|
|
181
|
-
|
|
182
|
-
message = exception.message || 'Unknown error'
|
|
183
|
-
backtrace = exception.backtrace || []
|
|
184
|
-
|
|
185
|
-
formatted = "#{exception.class}: #{message}"
|
|
186
|
-
if backtrace.any?
|
|
187
|
-
formatted += "\n" + backtrace.first(5).map { |line| " #{line}" }.join("\n")
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
formatted
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def generate_html(processed_tests, created_at, total_tests, passed_tests, failed_tests)
|
|
194
|
-
# Create safe IDs for method names (escape special chars for HTML/JS)
|
|
195
|
-
processed_tests.each do |test_class|
|
|
196
|
-
test_class[:methods].each do |method|
|
|
197
|
-
method[:safe_id] = method[:name].gsub(/['"]/, '').gsub(/[^a-zA-Z0-9]/, '_')
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
template_path = File.join(__dir__, 'templates', 'report.html.erb')
|
|
202
|
-
template = File.read(template_path)
|
|
203
|
-
erb = ERB.new(template)
|
|
204
|
-
|
|
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)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def generate_css
|
|
217
|
-
File.read(File.join(__dir__, 'assets', 'dashboard.css'))
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def generate_javascript
|
|
221
|
-
File.read(File.join(__dir__, 'assets', 'dashboard.js'))
|
|
222
|
-
end
|
|
223
98
|
end
|
|
224
99
|
end
|
|
225
100
|
end
|
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
<pre><%= h(step[:error]) %></pre>
|
|
64
64
|
</div>
|
|
65
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">
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
66
73
|
</div>
|
|
67
74
|
<% end %>
|
|
68
75
|
</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.
|
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.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Damon Clark
|
|
@@ -9,20 +9,6 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: rspec
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '3.0'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '3.0'
|
|
26
12
|
- !ruby/object:Gem::Dependency
|
|
27
13
|
name: rspec-rails
|
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,7 +38,7 @@ dependencies:
|
|
|
52
38
|
- !ruby/object:Gem::Version
|
|
53
39
|
version: '6.0'
|
|
54
40
|
description: CapyDash automatically generates clean, readable HTML test reports after
|
|
55
|
-
your RSpec suite finishes. Zero configuration required.
|
|
41
|
+
your RSpec or Minitest suite finishes. Zero configuration required.
|
|
56
42
|
email:
|
|
57
43
|
- dclark312@gmail.com
|
|
58
44
|
executables: []
|
|
@@ -64,9 +50,12 @@ files:
|
|
|
64
50
|
- lib/capydash.rb
|
|
65
51
|
- lib/capydash/assets/dashboard.css
|
|
66
52
|
- lib/capydash/assets/dashboard.js
|
|
53
|
+
- lib/capydash/minitest.rb
|
|
54
|
+
- lib/capydash/reporter.rb
|
|
67
55
|
- lib/capydash/rspec.rb
|
|
68
56
|
- lib/capydash/templates/report.html.erb
|
|
69
57
|
- lib/capydash/version.rb
|
|
58
|
+
- lib/minitest/capydash_plugin.rb
|
|
70
59
|
homepage: https://github.com/damonclark/capydash
|
|
71
60
|
licenses:
|
|
72
61
|
- MIT
|
|
@@ -88,5 +77,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
77
|
requirements: []
|
|
89
78
|
rubygems_version: 3.6.7
|
|
90
79
|
specification_version: 4
|
|
91
|
-
summary: Minimal static HTML report generator for RSpec system tests
|
|
80
|
+
summary: Minimal static HTML report generator for RSpec and Minitest system tests
|
|
92
81
|
test_files: []
|