capydash 0.2.5 → 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/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 +72 -0
- data/lib/capydash/reporter.rb +180 -0
- data/lib/capydash/rspec.rb +40 -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: 70d2bf9d0141c676e7685b951931458b7f224c210ae470de7a489b15d31018d8
|
|
4
|
+
data.tar.gz: b29754791193c6533d4c382585c0e7c2a489afe3a776eaec42d085c5fb601003
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c108d0f1310b218a43150e6d9421b19086353a23487af8b9fb08e479ededa947a4bb0fb35e6cd3acd6659c3afba008f698a665aa17d59bd948f3d1188300c3c7
|
|
7
|
+
data.tar.gz: b5e73a92af898a2ea0d1f4d876b61398084a018eb305e08427800152916ddc769d4d40966a97a552a64875d4672ce0d87969c90ec2bf93be4169448f740868ad
|
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,72 @@
|
|
|
1
|
+
require 'capydash/reporter'
|
|
2
|
+
|
|
3
|
+
module CapyDash
|
|
4
|
+
module Minitest
|
|
5
|
+
class Reporter < ::Minitest::AbstractReporter
|
|
6
|
+
include CapyDash::Reporter
|
|
7
|
+
|
|
8
|
+
def start
|
|
9
|
+
start_run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def record(result)
|
|
13
|
+
return unless @started_at
|
|
14
|
+
|
|
15
|
+
status = if result.skipped?
|
|
16
|
+
'pending'
|
|
17
|
+
elsif result.passed?
|
|
18
|
+
'passed'
|
|
19
|
+
else
|
|
20
|
+
'failed'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
error_message = nil
|
|
24
|
+
if result.failure
|
|
25
|
+
error_message = format_exception(result.failure)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
screenshot_path = nil
|
|
29
|
+
if status == 'failed'
|
|
30
|
+
# Rails system tests save screenshots before teardown to tmp/capybara/.
|
|
31
|
+
# By the time the reporter's record() runs, the session is torn down,
|
|
32
|
+
# so we look for the Rails-generated screenshot first.
|
|
33
|
+
screenshot_path = find_rails_screenshot(result.name) || capture_screenshot
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class_name = result.klass || 'UnknownTest'
|
|
37
|
+
method_name = result.name.to_s.sub(/\Atest_/, '').tr('_', ' ')
|
|
38
|
+
|
|
39
|
+
location = nil
|
|
40
|
+
if result.respond_to?(:source_location) && result.source_location
|
|
41
|
+
location = result.source_location.join(':')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
record_result({
|
|
45
|
+
class_name: class_name,
|
|
46
|
+
method_name: method_name,
|
|
47
|
+
status: status,
|
|
48
|
+
error: error_message,
|
|
49
|
+
location: location,
|
|
50
|
+
screenshot_path: screenshot_path
|
|
51
|
+
})
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def report
|
|
55
|
+
generate_report
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def passed?
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def find_rails_screenshot(test_name)
|
|
65
|
+
path = File.join(Dir.pwd, "tmp", "capybara", "failures_#{test_name}.png")
|
|
66
|
+
File.exist?(path) ? path : nil
|
|
67
|
+
rescue
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -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,47 @@ 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
|
+
|
|
38
|
+
execution_result = example.execution_result
|
|
39
|
+
|
|
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
|
|
49
|
+
|
|
50
|
+
error_message = nil
|
|
51
|
+
if execution_result.exception
|
|
52
|
+
error_message = format_exception(execution_result.exception)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
screenshot_path = nil
|
|
56
|
+
if status == 'failed'
|
|
57
|
+
screenshot_path = capture_screenshot
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class_name = extract_class_name(example)
|
|
61
|
+
|
|
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
|
+
})
|
|
70
|
+
end
|
|
71
|
+
|
|
140
72
|
private
|
|
141
73
|
|
|
142
74
|
def rspec_available?
|
|
@@ -147,19 +79,6 @@ module CapyDash
|
|
|
147
79
|
false
|
|
148
80
|
end
|
|
149
81
|
|
|
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
82
|
def extract_class_name(example)
|
|
164
83
|
group = example.metadata[:example_group]
|
|
165
84
|
while group && group[:parent_example_group]
|
|
@@ -175,51 +94,6 @@ module CapyDash
|
|
|
175
94
|
filename.split('_').map(&:capitalize).join('')
|
|
176
95
|
end
|
|
177
96
|
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
97
|
end
|
|
224
98
|
end
|
|
225
99
|
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.0
|
|
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: []
|