rspec-tracer 1.2.3 → 2.0.0.pre.2

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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +384 -67
  3. data/README.md +454 -429
  4. data/bin/rspec-tracer +15 -0
  5. data/lib/rspec_tracer/cache/Rakefile +43 -0
  6. data/lib/rspec_tracer/cli/cache_clear.rb +111 -0
  7. data/lib/rspec_tracer/cli/cache_info.rb +104 -0
  8. data/lib/rspec_tracer/cli/doctor.rb +284 -0
  9. data/lib/rspec_tracer/cli/explain.rb +158 -0
  10. data/lib/rspec_tracer/cli/report_open.rb +82 -0
  11. data/lib/rspec_tracer/cli.rb +116 -0
  12. data/lib/rspec_tracer/configuration.rb +1196 -3
  13. data/lib/rspec_tracer/engine.rb +1168 -0
  14. data/lib/rspec_tracer/example.rb +141 -11
  15. data/lib/rspec_tracer/filter.rb +35 -0
  16. data/lib/rspec_tracer/line_stub.rb +61 -0
  17. data/lib/rspec_tracer/load_config.rb +2 -2
  18. data/lib/rspec_tracer/logger.rb +15 -0
  19. data/lib/rspec_tracer/rails/README.md +78 -0
  20. data/lib/rspec_tracer/rails/i18n_tracking.rb +137 -0
  21. data/lib/rspec_tracer/rails/notifications.rb +263 -0
  22. data/lib/rspec_tracer/rails/preset.rb +94 -0
  23. data/lib/rspec_tracer/rails/railtie.rb +22 -0
  24. data/lib/rspec_tracer/rails.rb +15 -0
  25. data/lib/rspec_tracer/remote_cache/README.md +140 -0
  26. data/lib/rspec_tracer/remote_cache/Rakefile +35 -11
  27. data/lib/rspec_tracer/remote_cache/archive.rb +137 -0
  28. data/lib/rspec_tracer/remote_cache/backend.rb +73 -0
  29. data/lib/rspec_tracer/remote_cache/git_ancestry.rb +241 -0
  30. data/lib/rspec_tracer/remote_cache/local_fs_backend.rb +439 -0
  31. data/lib/rspec_tracer/remote_cache/redis_backend.rb +554 -0
  32. data/lib/rspec_tracer/remote_cache/s3_backend.rb +712 -0
  33. data/lib/rspec_tracer/remote_cache/user_tasks.rb +436 -0
  34. data/lib/rspec_tracer/remote_cache/validator.rb +40 -62
  35. data/lib/rspec_tracer/remote_cache.rb +22 -0
  36. data/lib/rspec_tracer/reporters/README.md +103 -0
  37. data/lib/rspec_tracer/reporters/base.rb +87 -0
  38. data/lib/rspec_tracer/reporters/coverage_json_reporter.rb +338 -0
  39. data/lib/rspec_tracer/reporters/html/.gitignore +19 -0
  40. data/lib/rspec_tracer/reporters/html/.prettierignore +4 -0
  41. data/lib/rspec_tracer/reporters/html/.prettierrc.json +9 -0
  42. data/lib/rspec_tracer/reporters/html/README.md +80 -0
  43. data/lib/rspec_tracer/reporters/html/dist/assets/index.css +2 -0
  44. data/lib/rspec_tracer/reporters/html/dist/assets/index.js +1 -0
  45. data/lib/rspec_tracer/reporters/html/dist/index.html +24 -0
  46. data/lib/rspec_tracer/reporters/html/eslint.config.js +62 -0
  47. data/lib/rspec_tracer/reporters/html/package-lock.json +4941 -0
  48. data/lib/rspec_tracer/reporters/html/package.json +29 -0
  49. data/lib/rspec_tracer/reporters/html/src/app.jsx +130 -0
  50. data/lib/rspec_tracer/reporters/html/src/components/AllExamples.jsx +86 -0
  51. data/lib/rspec_tracer/reporters/html/src/components/DuplicateExamples.jsx +68 -0
  52. data/lib/rspec_tracer/reporters/html/src/components/ExamplesDependency.jsx +78 -0
  53. data/lib/rspec_tracer/reporters/html/src/components/FilesDependency.jsx +72 -0
  54. data/lib/rspec_tracer/reporters/html/src/components/FlakyExamples.jsx +42 -0
  55. data/lib/rspec_tracer/reporters/html/src/components/ReportTable.jsx +131 -0
  56. data/lib/rspec_tracer/reporters/html/src/components/SearchBar.jsx +19 -0
  57. data/lib/rspec_tracer/reporters/html/src/index.html +23 -0
  58. data/lib/rspec_tracer/reporters/html/src/main.jsx +37 -0
  59. data/lib/rspec_tracer/reporters/html/src/styles.css +434 -0
  60. data/lib/rspec_tracer/reporters/html/vite.config.js +42 -0
  61. data/lib/rspec_tracer/reporters/html_reporter.rb +266 -0
  62. data/lib/rspec_tracer/reporters/json_reporter.rb +88 -0
  63. data/lib/rspec_tracer/reporters/payload_builder.rb +235 -0
  64. data/lib/rspec_tracer/reporters/registry.rb +120 -0
  65. data/lib/rspec_tracer/reporters/terminal_reporter.rb +264 -0
  66. data/lib/rspec_tracer/rspec/README.md +73 -0
  67. data/lib/rspec_tracer/rspec/installation.rb +97 -0
  68. data/lib/rspec_tracer/rspec/metadata.rb +96 -0
  69. data/lib/rspec_tracer/rspec/parallel_tests.rb +459 -0
  70. data/lib/rspec_tracer/rspec/reporter_hook.rb +84 -0
  71. data/lib/rspec_tracer/rspec/runner_hook.rb +239 -0
  72. data/lib/rspec_tracer/source_file.rb +24 -7
  73. data/lib/rspec_tracer/storage/README.md +35 -0
  74. data/lib/rspec_tracer/storage/backend.rb +130 -0
  75. data/lib/rspec_tracer/storage/json_backend.rb +884 -0
  76. data/lib/rspec_tracer/storage/lazy_snapshot.rb +65 -0
  77. data/lib/rspec_tracer/storage/schema.rb +50 -0
  78. data/lib/rspec_tracer/storage/serializer/json.rb +41 -0
  79. data/lib/rspec_tracer/storage/serializer/msgpack.rb +167 -0
  80. data/lib/rspec_tracer/storage/snapshot.rb +141 -0
  81. data/lib/rspec_tracer/storage/sqlite_backend.rb +693 -0
  82. data/lib/rspec_tracer/time_formatter.rb +37 -18
  83. data/lib/rspec_tracer/tracker/README.md +36 -0
  84. data/lib/rspec_tracer/tracker/coverage_adapter.rb +174 -0
  85. data/lib/rspec_tracer/tracker/declared_globs.rb +100 -0
  86. data/lib/rspec_tracer/tracker/dependency_graph.rb +134 -0
  87. data/lib/rspec_tracer/tracker/env_matcher.rb +127 -0
  88. data/lib/rspec_tracer/tracker/env_snapshot.rb +77 -0
  89. data/lib/rspec_tracer/tracker/example_registry.rb +153 -0
  90. data/lib/rspec_tracer/tracker/file_digest.rb +61 -0
  91. data/lib/rspec_tracer/tracker/filter.rb +127 -0
  92. data/lib/rspec_tracer/tracker/input.rb +99 -0
  93. data/lib/rspec_tracer/tracker/io_hooks/file.rb +55 -0
  94. data/lib/rspec_tracer/tracker/io_hooks/io.rb +24 -0
  95. data/lib/rspec_tracer/tracker/io_hooks/json.rb +23 -0
  96. data/lib/rspec_tracer/tracker/io_hooks/kernel.rb +26 -0
  97. data/lib/rspec_tracer/tracker/io_hooks/yaml.rb +38 -0
  98. data/lib/rspec_tracer/tracker/io_hooks.rb +195 -0
  99. data/lib/rspec_tracer/tracker/loaded_files_tracker.rb +295 -0
  100. data/lib/rspec_tracer/tracker/new_file_detector.rb +62 -0
  101. data/lib/rspec_tracer/tracker/whole_suite_invalidators.rb +96 -0
  102. data/lib/rspec_tracer/version.rb +4 -1
  103. data/lib/rspec_tracer.rb +231 -491
  104. metadata +94 -43
  105. data/lib/rspec_tracer/cache.rb +0 -207
  106. data/lib/rspec_tracer/coverage_merger.rb +0 -42
  107. data/lib/rspec_tracer/coverage_reporter.rb +0 -187
  108. data/lib/rspec_tracer/coverage_writer.rb +0 -58
  109. data/lib/rspec_tracer/html_reporter/Rakefile +0 -18
  110. data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +0 -56
  111. data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +0 -10881
  112. data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +0 -15381
  113. data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +0 -196
  114. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +0 -459
  115. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +0 -436
  116. data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +0 -92
  117. data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +0 -265
  118. data/lib/rspec_tracer/html_reporter/public/application.css +0 -5
  119. data/lib/rspec_tracer/html_reporter/public/application.js +0 -6
  120. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
  121. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
  122. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
  123. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
  124. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
  125. data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
  126. data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
  127. data/lib/rspec_tracer/html_reporter/reporter.rb +0 -242
  128. data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +0 -34
  129. data/lib/rspec_tracer/html_reporter/views/examples.erb +0 -58
  130. data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +0 -36
  131. data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +0 -36
  132. data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +0 -38
  133. data/lib/rspec_tracer/html_reporter/views/layout.erb +0 -38
  134. data/lib/rspec_tracer/remote_cache/aws.rb +0 -176
  135. data/lib/rspec_tracer/remote_cache/cache.rb +0 -75
  136. data/lib/rspec_tracer/remote_cache/repo.rb +0 -210
  137. data/lib/rspec_tracer/report_generator.rb +0 -158
  138. data/lib/rspec_tracer/report_merger.rb +0 -68
  139. data/lib/rspec_tracer/report_writer.rb +0 -141
  140. data/lib/rspec_tracer/reporter.rb +0 -204
  141. data/lib/rspec_tracer/rspec_reporter.rb +0 -41
  142. data/lib/rspec_tracer/rspec_runner.rb +0 -56
  143. data/lib/rspec_tracer/ruby_coverage.rb +0 -9
  144. data/lib/rspec_tracer/runner.rb +0 -278
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ # Reporters subsystem. Houses the default reporters (terminal,
5
+ # JSON, HTML, coverage.json) plus the protocol class
6
+ # ({Base}) that users subclass when shipping custom reporters
7
+ # via `add_reporter MyReporter`.
8
+ module Reporters
9
+ # Abstract base for every reporter rspec-tracer ships. Takes the
10
+ # finalized Storage::Snapshot + report_dir + run_metadata triplet
11
+ # (architectural decision (b), Option X - raw Snapshot, no
12
+ # projection struct) and exposes two lifecycle hooks:
13
+ #
14
+ # - `generate` - subclass must implement; emits the reporter's
15
+ # output format (JSON file, terminal lines, HTML etc.)
16
+ # - `no_op?` - true when the run had zero tracked examples; the
17
+ # Registry checks this before calling `generate` so empty runs
18
+ # don't litter the report_dir with empty artifacts.
19
+ #
20
+ # `initialize` accepts `**opts` so custom reporters can take
21
+ # constructor args via `config.add_reporter MyReporter, color: false`.
22
+ # The base class itself ignores opts; subclasses read what they need.
23
+ #
24
+ # Errors during `generate` are NOT rescued here - the Registry
25
+ # wraps each reporter call in a per-reporter rescue + warn, so a
26
+ # buggy custom reporter never propagates a non-zero exit into the
27
+ # user's test suite (graceful degradation, same contract as
28
+ # Storage backends per ARCHITECTURE.md).
29
+ #
30
+ # @example Registering a custom reporter
31
+ # class MyReporter < RSpecTracer::Reporters::Base
32
+ # def generate
33
+ # File.write(File.join(report_dir, 'my.json'),
34
+ # JSON.pretty_generate(snapshot.to_h))
35
+ # end
36
+ # end
37
+ #
38
+ # RSpecTracer.configure do
39
+ # add_reporter MyReporter, my_option: true
40
+ # end
41
+ class Base
42
+ # @return [RSpecTracer::Storage::Snapshot, nil] finalized run snapshot.
43
+ # @return [String] absolute report directory.
44
+ # @return [Hash] run_metadata hash (pid / run_time / etc).
45
+ # @return [#info, #warn, nil] tracer logger.
46
+ # @return [Hash] reporter-specific options.
47
+ attr_reader :snapshot, :report_dir, :run_metadata, :logger, :options
48
+
49
+ # @param snapshot [RSpecTracer::Storage::Snapshot] the finalized
50
+ # per-run snapshot (`nil` only on early-abort paths)
51
+ # @param report_dir [String] absolute path the reporter should
52
+ # write into (created by Engine.finalize before this fires)
53
+ # @param run_metadata [Hash] pid / run_time / started_at /
54
+ # cache_path / parallel_tests / rails flags
55
+ # @param logger [#info, #warn, nil] tracer logger
56
+ # @param options [Hash] reporter-specific keyword args from
57
+ # `add_reporter MyReporter, **opts`
58
+ def initialize(snapshot:, report_dir:, run_metadata:, logger: nil, **options)
59
+ @snapshot = snapshot
60
+ @report_dir = report_dir
61
+ @run_metadata = run_metadata || {}
62
+ @logger = logger
63
+ @options = options
64
+ end
65
+
66
+ # Subclass must implement. Called once per reporter per run by
67
+ # the Registry. Errors propagate to the Registry's per-reporter
68
+ # rescue (graceful degradation: a buggy reporter never breaks
69
+ # the suite).
70
+ #
71
+ # @return [void]
72
+ def generate
73
+ raise NotImplementedError, "#{self.class}#generate must be implemented"
74
+ end
75
+
76
+ # Registry skips `generate` when this returns true so empty runs
77
+ # do not produce empty artifacts.
78
+ #
79
+ # @return [Boolean]
80
+ def no_op?
81
+ snapshot.nil? ||
82
+ snapshot.all_examples.nil? ||
83
+ snapshot.all_examples.empty?
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'set'
6
+
7
+ require_relative 'base'
8
+ require_relative '../line_stub'
9
+
10
+ module RSpecTracer
11
+ # Internal Reporters — see {RSpecTracer} for the user-facing surface.
12
+ # @api private
13
+ module Reporters
14
+ # Single owner of `coverage.json` emission in 2.0. Replaces the
15
+ # legacy CoverageReporter + CoverageWriter + CoverageMerger +
16
+ # RubyCoverage quartet that 1.x carried. Output shape preserved byte-for-byte
17
+ # for downstream consumers (user CI dashboards parse this):
18
+ #
19
+ # { "RSpecTracer": {
20
+ # "coverage": { "<absolute_path>": [Integer|nil, ...], ... },
21
+ # "timestamp": <Integer>
22
+ # } }
23
+ #
24
+ # Cumulative coverage source: `::Coverage.peek_result` at finalize
25
+ # time, routed through `Tracker::CoverageAdapter#peek_unfiltered`
26
+ # so the lib/-wide peek_result call-site count stays at 3 (one
27
+ # each in coverage_adapter.rb, rspec/installation.rb, and
28
+ # tracker/loaded_files_tracker.rb).
29
+ #
30
+ # Skipped-example contribution: `Engine#merge_skipped_coverage`,
31
+ # which replays 1.x's CoverageReporter algorithm verbatim - per-
32
+ # skipped-example per-line strengths add into the cumulative map.
33
+ #
34
+ # SimpleCov interop: when SimpleCov is loaded, this reporter does
35
+ # NOT write coverage.json. Instead it installs a small inner module
36
+ # via `::Coverage.singleton_class.prepend` so SimpleCov's at_exit
37
+ # result-merge sees rspec-tracer's filtered coverage. Matches 1.x
38
+ # behavior + the integration matrix convention.
39
+ #
40
+ # Parallel_tests: per-worker emit happens through the Registry
41
+ # like every other reporter (each worker writes its own
42
+ # coverage.json under the per-worker coverage_path). The elected
43
+ # worker calls `merge_parallel` from `RSpec::ParallelTests` to
44
+ # union the per-worker files into the top-level coverage.json.
45
+ class CoverageJsonReporter < Base
46
+ # Internal constant.
47
+ # @api private
48
+ FILENAME = 'coverage.json'
49
+
50
+ # Concrete implementation of {RSpecTracer::Reporters::Base#generate}.
51
+ # Builds the cumulative coverage payload and writes `coverage.json`
52
+ # under {#report_dir}, except when SimpleCov owns the run (in which
53
+ # case the SimpleCov interop shim takes over).
54
+ #
55
+ # @return [String, nil] absolute path of the written file, or nil
56
+ # when the run wrote nothing (engine missing / SimpleCov interop).
57
+ def generate
58
+ return nil unless RSpecTracer.engine
59
+ return install_simplecov_interop if RSpecTracer.simplecov?
60
+
61
+ coverage = build_coverage
62
+ write_coverage_json(coverage)
63
+ log_stats(coverage)
64
+ target_path
65
+ end
66
+
67
+ # Coverage emission fires unconditionally - even when no examples
68
+ # ran, 1.x writes coverage.json with whatever boot-time peek_result
69
+ # returned (plus filter + stub). Override Base#no_op? so the
70
+ # parent's "snapshot has zero examples" early-return doesn't gate
71
+ # this emitter.
72
+ def no_op?
73
+ false
74
+ end
75
+
76
+ # Class-level rollup for parallel_tests. Unions per-line
77
+ # strengths across the per-worker coverage.json files written by
78
+ # `generate` on each worker, then writes the merged top-level
79
+ # coverage.json. Replaces 1.x's
80
+ # `CoverageMerger.new + CoverageWriter.new(top_path, merger)`.
81
+ def self.merge_parallel(peer_paths:, output_path:, logger: nil)
82
+ merged = {}
83
+ peer_paths.each do |peer|
84
+ path = File.join(peer, FILENAME)
85
+ next unless File.file?(path)
86
+
87
+ data = JSON.parse(File.read(path, encoding: 'UTF-8'))
88
+ peer_coverage = data.fetch('RSpecTracer').fetch('coverage')
89
+ peer_coverage.each do |file_path, lines|
90
+ if merged.key?(file_path)
91
+ merge_line_arrays(merged[file_path], lines)
92
+ else
93
+ merged[file_path] = lines.dup
94
+ end
95
+ end
96
+ end
97
+
98
+ FileUtils.mkdir_p(File.dirname(output_path))
99
+ payload = { RSpecTracer: { coverage: merged, timestamp: Time.now.utc.to_i } }
100
+ File.write(output_path, JSON.pretty_generate(payload), encoding: 'UTF-8')
101
+ logger&.debug("rspec-tracer: wrote merged coverage.json to #{output_path}")
102
+ output_path
103
+ end
104
+
105
+ # Internal helper for the tracer pipeline.
106
+ # @api private
107
+ def self.merge_line_arrays(into, from)
108
+ from.each_with_index do |strength, idx|
109
+ next if strength.nil? || into[idx].nil?
110
+
111
+ into[idx] += strength
112
+ end
113
+ end
114
+ private_class_method :merge_line_arrays
115
+
116
+ private
117
+
118
+ # Internal method on the tracer pipeline.
119
+ # @api private
120
+ def build_coverage
121
+ coverage = engine.coverage_adapter.peek_unfiltered
122
+ accumulate_skipped(coverage)
123
+ finalize_coverage_files(coverage)
124
+ end
125
+
126
+ # Shape-agnostic skipped-coverage accumulator. The `coverage`
127
+ # hash carries one of two per-file shapes interchangeably:
128
+ #
129
+ # - `Array<Integer|nil>` (lines-only — peek_unfiltered;
130
+ # the user-facing `coverage.json`
131
+ # contract)
132
+ # - `{ lines: Array, branches: Hash }`
133
+ # (hash-mode — peek_unfiltered_full;
134
+ # what SimpleCov needs when branch
135
+ # coverage is enabled)
136
+ #
137
+ # Both callers (`build_coverage` writing coverage.json,
138
+ # `install_simplecov_interop` feeding SimpleCov.result) share
139
+ # this one accumulator; the per-shape mechanics are isolated in
140
+ # `ensure_mutable_lines`. Hash-mode entries keep their
141
+ # `:branches` sub-hash unchanged (this method only touches
142
+ # lines); array-mode entries are mutated in place. New entries
143
+ # synthesised for files Coverage didn't observe go in as Array
144
+ # (line stubs have no branches to preserve).
145
+ def accumulate_skipped(coverage)
146
+ skipped_ids = engine.registry.ids_with_status(:skipped)
147
+ return coverage if skipped_ids.empty?
148
+
149
+ engine.merge_skipped_coverage(skipped_ids).each do |file_path, line_map|
150
+ line_arr = ensure_mutable_lines(coverage, file_path)
151
+ line_map.each do |line_number, strength|
152
+ idx = line_number.to_i
153
+ line_arr[idx] = (line_arr[idx] || 0) + strength
154
+ end
155
+ end
156
+ coverage
157
+ end
158
+
159
+ # Returns the mutable `Array<Integer|nil>` view for
160
+ # `coverage[file_path]` we can write per-line strengths into,
161
+ # rewriting `coverage[file_path]` in-place when a dup or stub
162
+ # was needed. `Coverage.peek_result` returns frozen line arrays
163
+ # on Ruby 3.2+; dup-on-write so subsequent writes share the
164
+ # mutable copy.
165
+ #
166
+ # nil → install line_stub (Array shape; new entry
167
+ # has no branches to preserve).
168
+ # Hash{lines:,} → if `:lines` is frozen / nil, replace the
169
+ # Hash entry with one carrying a fresh /
170
+ # dup'd lines array + the original branches.
171
+ # Array → if frozen, dup + replace; else mutate in
172
+ # place.
173
+ def ensure_mutable_lines(coverage, file_path)
174
+ existing = coverage[file_path]
175
+
176
+ case existing
177
+ when nil
178
+ stub = line_stub(file_path)
179
+ coverage[file_path] = stub
180
+ stub
181
+ when ::Hash
182
+ line_arr = existing[:lines]
183
+ return line_arr if line_arr && !line_arr.frozen?
184
+
185
+ line_arr = line_arr ? line_arr.dup : line_stub(file_path)
186
+ coverage[file_path] = { lines: line_arr, branches: existing[:branches] || {} }
187
+ line_arr
188
+ else
189
+ return existing unless existing.frozen?
190
+
191
+ dup = existing.dup
192
+ coverage[file_path] = dup
193
+ dup
194
+ end
195
+ end
196
+
197
+ # Apply `coverage_filters` + `coverage_tracked_files` glob; sort
198
+ # the resulting file list (matches legacy CoverageReporter#
199
+ # final_coverage_files at coverage_reporter.rb:113-131) and slice
200
+ # + line-stub missing entries.
201
+ def finalize_coverage_files(coverage)
202
+ all = coverage.keys.to_set
203
+
204
+ if RSpecTracer.coverage_tracked_files
205
+ Dir[RSpecTracer.coverage_tracked_files].each do |name|
206
+ all << File.expand_path(name, RSpecTracer.root)
207
+ end
208
+ end
209
+
210
+ all.select! do |file_path|
211
+ name = file_name_for(file_path)
212
+ RSpecTracer.coverage_filters.none? { |filter| filter.match?(file_name: name) }
213
+ end
214
+
215
+ all.sort.to_h { |file_path| [file_path, coverage[file_path] || line_stub(file_path).freeze] }
216
+ end
217
+
218
+ # Internal method on the tracer pipeline.
219
+ # @api private
220
+ def file_name_for(file_path)
221
+ prefix = "#{RSpecTracer.root}/"
222
+ return file_path unless file_path.start_with?(prefix)
223
+
224
+ file_path.sub(/\A#{Regexp.escape(RSpecTracer.root)}/, '')
225
+ end
226
+
227
+ # Internal method on the tracer pipeline.
228
+ # @api private
229
+ def write_coverage_json(coverage)
230
+ path = target_path
231
+ FileUtils.mkdir_p(File.dirname(path))
232
+ payload = { RSpecTracer: { coverage: coverage, timestamp: Time.now.utc.to_i } }
233
+ File.write(path, JSON.pretty_generate(payload), encoding: 'UTF-8')
234
+ end
235
+
236
+ # Internal method on the tracer pipeline.
237
+ # @api private
238
+ def target_path
239
+ File.join(RSpecTracer.coverage_path, FILENAME)
240
+ end
241
+
242
+ # Internal method on the tracer pipeline.
243
+ # @api private
244
+ def log_stats(coverage)
245
+ total = 0
246
+ covered = 0
247
+ coverage.each_value do |lines|
248
+ lines.each do |strength|
249
+ next if strength.nil?
250
+
251
+ total += 1
252
+ covered += 1 if strength.positive?
253
+ end
254
+ end
255
+ return if total.zero?
256
+
257
+ percent = (100.0 * covered / total).round(2)
258
+ logger&.info(
259
+ "rspec-tracer: coverage #{covered}/#{total} LOC (#{percent}%) -> #{target_path}"
260
+ )
261
+ end
262
+
263
+ # Delegates to the top-level RSpecTracer::LineStub which holds
264
+ # the per-engine MRI/JRuby implementations. Kept off the gated
265
+ # path so the JRuby branch (uncoverable on MRI) doesn't fail
266
+ # the 100%-line+branch contract this gated subdir carries.
267
+ def line_stub(file_path)
268
+ RSpecTracer::LineStub.for(file_path)
269
+ end
270
+
271
+ def engine
272
+ RSpecTracer.engine
273
+ end
274
+
275
+ # SimpleCov-branch coverage payload skips coverage_filters +
276
+ # line_stub. 1.x parity: when SimpleCov is loaded, the legacy
277
+ # CoverageReporter.coverage exposed via the RubyCoverage shim
278
+ # is the raw peek + skipped-merge accumulator, NOT the
279
+ # generate_final_coverage post-filter view (the legacy
280
+ # `simplecov? ? run_simplecov_exit_task : run_coverage_exit_task`
281
+ # branch only ran generate_final_coverage on the non-SimpleCov
282
+ # side). Preserving this shape keeps SimpleCov's at_exit
283
+ # result-merge seeing whatever it was tracking minus rspec-
284
+ # tracer's narrow filters.
285
+ #
286
+ # `peek_unfiltered_full` preserves Coverage's full
287
+ # `{lines:, branches:}` hash shape verbatim (vs `peek_unfiltered`
288
+ # which reduces hash-mode entries to `:lines` only for the
289
+ # user-facing `coverage.json` Array<Integer|nil> contract).
290
+ # SimpleCov's branch report needs the branches sub-hash;
291
+ # silently stripping them here was the pre-PR-#165 bug that
292
+ # made every dogfooded suite report `Branch Coverage: 0/0`
293
+ # under `enable_coverage :branch`.
294
+ #
295
+ # `accumulate_skipped` is shape-agnostic (handles both
296
+ # hash-mode and array-mode entries; preserves `:branches`
297
+ # untouched on hash-mode dup-on-write), so we hand it the full
298
+ # peek directly without any view-extraction round-trip. Empty
299
+ # skipped registry → it early-returns + nothing reallocates;
300
+ # non-empty → only the per-skipped-file line arrays get dup'd.
301
+ # Either way, no per-call O(N files) allocation.
302
+ def install_simplecov_interop
303
+ coverage = engine.coverage_adapter.peek_unfiltered_full
304
+ accumulate_skipped(coverage)
305
+ SimpleCovInterop.install(coverage)
306
+ nil
307
+ end
308
+
309
+ # Inner module preserving 1.x's RubyCoverage shim contract. When
310
+ # SimpleCov calls `::Coverage.result` at its at_exit time, this
311
+ # prepended `result` method returns rspec-tracer's filtered
312
+ # cumulative coverage so SimpleCov's result-merge sees exactly
313
+ # what rspec-tracer saw. Matches the 1.x behavior the
314
+ # integration matrix relies on.
315
+ module SimpleCovInterop
316
+ class << self
317
+ # Internal attribute.
318
+ # @api private
319
+ attr_accessor :coverage
320
+ end
321
+
322
+ # Internal helper for the tracer pipeline.
323
+ # @api private
324
+ def self.install(coverage)
325
+ self.coverage = coverage
326
+ klass = ::Coverage.singleton_class
327
+ klass.prepend(self) unless klass.ancestors.include?(self)
328
+ end
329
+
330
+ # Internal method on the tracer pipeline.
331
+ # @api private
332
+ def result
333
+ SimpleCovInterop.coverage
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,19 @@
1
+ # Per-subdirectory gitignore for the HTML reporter frontend toolchain.
2
+ #
3
+ # Committed: src/, dist/, package.json, package-lock.json,
4
+ # vite.config.js, eslint.config.js, .prettierrc.json,
5
+ # .prettierignore, README.md.
6
+ # Not committed: node_modules/ (reproduced from package-lock.json),
7
+ # .vite/ (local Vite cache), build/test artifacts.
8
+
9
+ node_modules/
10
+ .vite/
11
+ .vite-cache/
12
+ coverage/
13
+ *.log
14
+ npm-debug.log*
15
+ yarn-debug.log*
16
+ yarn-error.log*
17
+ pnpm-debug.log*
18
+ .eslintcache
19
+ .DS_Store
@@ -0,0 +1,4 @@
1
+ dist/
2
+ node_modules/
3
+ coverage/
4
+ package-lock.json
@@ -0,0 +1,9 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "trailingComma": "es5",
6
+ "semi": true,
7
+ "arrowParens": "always",
8
+ "endOfLine": "lf"
9
+ }
@@ -0,0 +1,80 @@
1
+ # HTML reporter frontend
2
+
3
+ Source for the single-page HTML report rendered by
4
+ `RSpecTracer::Reporters::HtmlReporter` into
5
+ `<report_dir>/index.html` at finalize-time.
6
+
7
+ ## Stack
8
+
9
+ - **Preact 10** — React-identical API at ~3 KB runtime. Five report
10
+ types share a `ReportTable` / `SearchBar` pair, so a component tree
11
+ pays for itself. Users who fork to customize find a universal API;
12
+ end users who open the report get a tiny bundle.
13
+ - **Vite** — zero-config JSX + CSS pipeline, deterministic output.
14
+ - **No TypeScript** — plain JSX keeps the toolchain surface small. Add
15
+ `.ts` if a future widget earns it.
16
+
17
+ No test framework on the frontend side. The single source of truth for
18
+ rendered output is the Ruby-side golden spec
19
+ (`spec/fixtures/golden/html_reporter/index.html`) which compares the
20
+ byte-identical output of `HtmlReporter#generate` against a committed
21
+ baseline. Drift in components shows up there.
22
+
23
+ ## Layout
24
+
25
+ ```
26
+ lib/rspec_tracer/reporters/html/
27
+ ├── package.json # dependencies pinned, lock committed
28
+ ├── package-lock.json
29
+ ├── vite.config.js # stable filenames, no hashing, deterministic
30
+ ├── README.md # this file
31
+ ├── src/
32
+ │ ├── index.html # template; Ruby injects payload + fallback
33
+ │ ├── main.jsx # boot + hydrate
34
+ │ ├── app.jsx # tab shell + summary
35
+ │ ├── styles.css # all styles (single bundle)
36
+ │ └── components/
37
+ │ ├── ReportTable.jsx # shared primitive: filter + sort
38
+ │ ├── SearchBar.jsx
39
+ │ ├── AllExamples.jsx
40
+ │ ├── DuplicateExamples.jsx
41
+ │ ├── FlakyExamples.jsx
42
+ │ ├── ExamplesDependency.jsx
43
+ │ └── FilesDependency.jsx
44
+ └── dist/ # COMMITTED build output; users never rebuild
45
+ ├── index.html
46
+ └── assets/
47
+ ├── index.js
48
+ └── index.css
49
+ ```
50
+
51
+ ## Rebuilding
52
+
53
+ ```
54
+ task reporters:html:build # npm ci + vite build
55
+ ```
56
+
57
+ The Taskfile wires this into `task gem:build` so local gem builds
58
+ always pick up a fresh dist. On CI, the drift check
59
+ (`task reporters:html:check`) runs a fresh build and fails if
60
+ `git diff --exit-code dist/` is non-empty — committing a src/ change
61
+ without rebuilding fails fast.
62
+
63
+ ## Integration with the Ruby reporter
64
+
65
+ At finalize-time, `HtmlReporter#generate`:
66
+
67
+ 1. Reads `dist/index.html` (skeleton with marker comments).
68
+ 2. Replaces `<!-- RSPEC_TRACER_FALLBACK -->` with server-rendered
69
+ `<table>` HTML for each of the 5 report types. These tables
70
+ satisfy the "works without JavaScript" AC and are removed from
71
+ the DOM by `main.jsx` after Preact hydrates.
72
+ 3. Replaces the `<script id="report-data">` body with the reporter
73
+ payload (built by `Reporters::PayloadBuilder`, the same module
74
+ `JsonReporter` uses).
75
+ 4. Writes the finished HTML to `<report_dir>/index.html` and copies
76
+ `dist/assets/` to `<report_dir>/assets/`.
77
+
78
+ The reporter emits identically whether JavaScript is available in the
79
+ end user's browser or not. The interactive version is strictly
80
+ additive.
@@ -0,0 +1,2 @@
1
+ :root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#f7f8fa;--surface:#fff;--surface-alt:#f0f3f7;--border:#d8dde4;--border-strong:#b0b8c2;--text:#1b1f26;--text-muted:#515b66;--accent:#1a5fb4;--accent-contrast:#fff;--danger:#a6160f;--warning:#8a5a00;--success:#146c2e;--neutral:#4a5460;--skipped:#5f6774;--flaky:#8a3fa6;--focus-ring:#1a5fb4}*{box-sizing:border-box}html,body{background:var(--bg);color:var(--text);margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica Neue,Arial,sans-serif;font-size:14px;line-height:1.45}code{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.92em}a{color:var(--accent)}.noscript-banner{color:#6a4a00;background:#fff4d6;border-bottom:1px solid #e0c98a;padding:12px 20px;font-size:13px}.report-root{max-width:1400px;margin:0 auto;padding:24px}.report-header h1{margin:0 0 12px;font-size:22px}.summary{flex-wrap:wrap;gap:8px;margin:0 0 12px;padding:0;display:flex}.summary-item{background:var(--surface);border:1px solid var(--border);border-radius:6px;flex-direction:column;gap:2px;min-width:90px;padding:10px 14px;display:flex}.summary-item dt{text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted);margin:0;font-size:11px}.summary-item dd{margin:0;font-size:20px;font-weight:600}.summary-passed dd{color:var(--success)}.summary-failed dd{color:var(--danger)}.summary-pending dd{color:var(--warning)}.summary-skipped dd{color:var(--skipped)}.report-meta{color:var(--text-muted);flex-wrap:wrap;gap:16px;margin:0 0 20px;font-size:12px;display:flex}.tab-bar{border-bottom:1px solid var(--border);flex-wrap:wrap;gap:4px;margin-bottom:16px;display:flex}.tab-button{appearance:none;color:var(--text);cursor:pointer;font:inherit;background:0 0;border:1px solid #0000;border-bottom:none;border-top-left-radius:6px;border-top-right-radius:6px;align-items:center;gap:8px;padding:8px 14px;display:inline-flex}.tab-button:hover{background:var(--surface);border-color:var(--border)}.tab-button.is-active{background:var(--surface);border-color:var(--border);border-bottom-color:var(--surface);margin-bottom:-1px;font-weight:600}.tab-button:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.tab-count{background:var(--surface-alt);color:var(--text-muted);border-radius:10px;padding:1px 8px;font-size:11px}.tab-button.is-active .tab-count{background:var(--border);color:var(--text)}.report-panel{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:20px}.report-table{flex-direction:column;gap:12px;display:flex}.search-bar{flex-direction:column;gap:4px;max-width:360px;display:flex}.search-label{text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted);font-size:11px}.search-input{appearance:none;border:1px solid var(--border);font:inherit;background:var(--surface);color:var(--text);border-radius:4px;padding:6px 10px}.search-input:focus-visible{outline:2px solid var(--focus-ring);outline-offset:1px;border-color:var(--focus-ring)}.report-table__counts{color:var(--text-muted);margin:0;font-size:12px}.data-table{border-collapse:collapse;border:1px solid var(--border);width:100%}.data-table th,.data-table td{border-bottom:1px solid var(--border);text-align:left;vertical-align:top;padding:8px 12px}.data-table thead th{background:var(--surface-alt);color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border-strong);font-size:11px;font-weight:600;position:sticky;top:0}.data-table tbody tr:nth-child(2n){background:var(--surface-alt)}.data-table .is-numeric{text-align:right;font-variant-numeric:tabular-nums}.sort-button{appearance:none;color:inherit;font:inherit;cursor:pointer;text-transform:inherit;letter-spacing:inherit;background:0 0;border:none;align-items:center;gap:4px;padding:0;display:inline-flex}.sort-button:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.sort-indicator{color:var(--text-muted);font-size:10px}.sort-button[data-sort=asc] .sort-indicator,.sort-button[data-sort=desc] .sort-indicator{color:var(--text)}.report-table__empty{text-align:center;color:var(--text-muted);padding:20px;font-style:italic}.cell-description{max-width:520px}.cell-location,.cell-file,.cell-id{color:var(--text-muted)}.cell-deps{max-width:520px}.badge{text-transform:uppercase;letter-spacing:.04em;color:var(--accent-contrast);border-radius:10px;padding:2px 8px;font-size:11px;font-weight:600;display:inline-block}.status-passed{background:var(--success)}.status-failed{background:var(--danger)}.status-pending{background:var(--warning)}.status-skipped{background:var(--skipped)}.status-flaky{background:var(--flaky)}.status-unknown{background:var(--neutral)}.deps-details{margin:0}.deps-details+.deps-details{margin-top:6px}.deps-details summary{cursor:pointer;color:var(--accent)}.deps-details summary::marker{color:var(--text-muted)}.deps-list{margin:6px 0 0;padding-left:18px}.deps-list li{align-items:baseline;gap:8px;padding:2px 0;display:flex}.dep-count{color:var(--text-muted);font-size:12px}.visually-hidden{clip:rect(0, 0, 0, 0)!important;white-space:nowrap!important;border:0!important;width:1px!important;height:1px!important;margin:-1px!important;padding:0!important;position:absolute!important;overflow:hidden!important}.fallback-root{flex-direction:column;gap:24px;padding:0 0 24px;display:flex}.fallback-section h2{margin:0 0 8px;font-size:16px}.fallback-table{border-collapse:collapse;border:1px solid var(--border);width:100%}.fallback-table caption{text-align:left;color:var(--text-muted);padding:6px 0;font-size:12px}.fallback-table th,.fallback-table td{border-bottom:1px solid var(--border);text-align:left;vertical-align:top;padding:6px 10px}.fallback-table thead th{background:var(--surface-alt);text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted);font-size:11px}@media (prefers-reduced-motion:reduce){*{transition-duration:.01ms!important;animation-duration:.01ms!important}}
2
+ /*$vite$:1*/
@@ -0,0 +1 @@
1
+ (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e,t,n,r,i,a,o,s,c,l,u,d,f,p,m={},h=[],g=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,_=Array.isArray;function v(e,t){for(var n in t)e[n]=t[n];return e}function y(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function b(t,n,r){var i,a,o,s={};for(o in n)o==`key`?i=n[o]:o==`ref`?a=n[o]:s[o]=n[o];if(arguments.length>2&&(s.children=arguments.length>3?e.call(arguments,2):r),typeof t==`function`&&t.defaultProps!=null)for(o in t.defaultProps)s[o]===void 0&&(s[o]=t.defaultProps[o]);return x(t,s,i,a,null)}function x(e,r,i,a,o){var s={type:e,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o??++n,__i:-1,__u:0};return o==null&&t.vnode!=null&&t.vnode(s),s}function S(e){return e.children}function C(e,t){this.props=e,this.context=t}function w(e,t){if(t==null)return e.__?w(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type==`function`?w(e):null}function T(e){if(e.__P&&e.__d){var n=e.__v,r=n.__e,i=[],a=[],o=v({},n);o.__v=n.__v+1,t.vnode&&t.vnode(o),P(e.__P,o,n,e.__n,e.__P.namespaceURI,32&n.__u?[r]:null,i,r??w(n),!!(32&n.__u),a),o.__v=n.__v,o.__.__k[o.__i]=o,ne(i,o,a),n.__e=n.__=null,o.__e!=r&&E(o)}}function E(e){if((e=e.__)!=null&&e.__c!=null)return e.__e=e.__c.base=null,e.__k.some(function(t){if(t!=null&&t.__e!=null)return e.__e=e.__c.base=t.__e}),E(e)}function D(e){(!e.__d&&(e.__d=!0)&&r.push(e)&&!O.__r++||i!=t.debounceRendering)&&((i=t.debounceRendering)||a)(O)}function O(){try{for(var e,t=1;r.length;)r.length>t&&r.sort(o),e=r.shift(),t=r.length,T(e)}finally{r.length=O.__r=0}}function ee(e,t,n,r,i,a,o,s,c,l,u){var d,f,p,g,_,v,y,b=r&&r.__k||h,x=t.length;for(c=k(n,t,b,c,x),d=0;d<x;d++)(p=n.__k[d])!=null&&(f=p.__i!=-1&&b[p.__i]||m,p.__i=d,v=P(e,p,f,i,a,o,s,c,l,u),g=p.__e,p.ref&&f.ref!=p.ref&&(f.ref&&I(f.ref,null,p),u.push(p.ref,p.__c||g,p)),_==null&&g!=null&&(_=g),(y=!!(4&p.__u))||f.__k===p.__k?(c=A(p,c,e,y),y&&f.__e&&(f.__e=null)):typeof p.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),p.__u&=-7);return n.__e=_,c}function k(e,t,n,r,i){var a,o,s,c,l,u=n.length,d=u,f=0;for(e.__k=Array(i),a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o==`string`||typeof o==`number`||typeof o==`bigint`||o.constructor==String?o=e.__k[a]=x(null,o,null,null,null):_(o)?o=e.__k[a]=x(S,{children:o},null,null,null):o.constructor===void 0&&o.__b>0?o=e.__k[a]=x(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):e.__k[a]=o,c=a+f,o.__=e,o.__b=e.__b+1,s=null,(l=o.__i=j(o,n,c,d))!=-1&&(d--,(s=n[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(i>u?f--:i<u&&f++),typeof o.type!=`function`&&(o.__u|=4)):l!=c&&(l==c-1?f--:l==c+1?f++:(l>c?f--:f++,o.__u|=4))):e.__k[a]=null;if(d)for(a=0;a<u;a++)(s=n[a])!=null&&!(2&s.__u)&&(s.__e==r&&(r=w(s)),L(s,s));return r}function A(e,t,n,r){var i,a;if(typeof e.type==`function`){for(i=e.__k,a=0;i&&a<i.length;a++)i[a]&&(i[a].__=e,t=A(i[a],t,n,r));return t}e.__e!=t&&(r&&(t&&e.type&&!t.parentNode&&(t=w(e)),n.insertBefore(e.__e,t||null)),t=e.__e);do t&&=t.nextSibling;while(t!=null&&t.nodeType==8);return t}function j(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l!=null&&(2&l.__u)==0;if(l===null&&s==null||u&&s==l.key&&c==l.type)return n;if(r>+!!u){for(i=n-1,a=n+1;i>=0||a<t.length;)if((l=t[o=i>=0?i--:a++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function M(e,t,n){t[0]==`-`?e.setProperty(t,n??``):e[t]=n==null?``:typeof n!=`number`||g.test(t)?n:n+`px`}function N(e,t,n,r,i){var a,o;n:if(t==`style`)if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||M(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||M(e.style,t,n[t])}else if(t[0]==`o`&&t[1]==`n`)a=t!=(t=t.replace(u,`$1`)),o=t.toLowerCase(),t=o in e||t==`onFocusOut`||t==`onFocusIn`?o.slice(2):t.slice(2),e.l||={},e.l[t+a]=n,n?r?n[l]=r[l]:(n[l]=d,e.addEventListener(t,a?p:f,a)):e.removeEventListener(t,a?p:f,a);else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function te(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n[c]==null)n[c]=d++;else if(n[c]<r[l])return;return r(t.event?t.event(n):n)}}}function P(e,n,r,i,a,o,s,c,l,u){var d,f,p,m,g,b,x,w,T,E,D,O,k,A,j,M=n.type;if(n.constructor!==void 0)return null;128&r.__u&&(l=!!(32&r.__u),o=[c=n.__e=r.__e]),(d=t.__b)&&d(n);n:if(typeof M==`function`)try{if(w=n.props,T=M.prototype&&M.prototype.render,E=(d=M.contextType)&&i[d.__c],D=d?E?E.props.value:d.__:i,r.__c?x=(f=n.__c=r.__c).__=f.__E:(T?n.__c=f=new M(w,D):(n.__c=f=new C(w,D),f.constructor=M,f.render=ae),E&&E.sub(f),f.state||={},f.__n=i,p=f.__d=!0,f.__h=[],f._sb=[]),T&&f.__s==null&&(f.__s=f.state),T&&M.getDerivedStateFromProps!=null&&(f.__s==f.state&&(f.__s=v({},f.__s)),v(f.__s,M.getDerivedStateFromProps(w,f.__s))),m=f.props,g=f.state,f.__v=n,p)T&&M.getDerivedStateFromProps==null&&f.componentWillMount!=null&&f.componentWillMount(),T&&f.componentDidMount!=null&&f.__h.push(f.componentDidMount);else{if(T&&M.getDerivedStateFromProps==null&&w!==m&&f.componentWillReceiveProps!=null&&f.componentWillReceiveProps(w,D),n.__v==r.__v||!f.__e&&f.shouldComponentUpdate!=null&&!1===f.shouldComponentUpdate(w,f.__s,D)){n.__v!=r.__v&&(f.props=w,f.state=f.__s,f.__d=!1),n.__e=r.__e,n.__k=r.__k,n.__k.some(function(e){e&&(e.__=n)}),h.push.apply(f.__h,f._sb),f._sb=[],f.__h.length&&s.push(f);break n}f.componentWillUpdate!=null&&f.componentWillUpdate(w,f.__s,D),T&&f.componentDidUpdate!=null&&f.__h.push(function(){f.componentDidUpdate(m,g,b)})}if(f.context=D,f.props=w,f.__P=e,f.__e=!1,O=t.__r,k=0,T)f.state=f.__s,f.__d=!1,O&&O(n),d=f.render(f.props,f.state,f.context),h.push.apply(f.__h,f._sb),f._sb=[];else do f.__d=!1,O&&O(n),d=f.render(f.props,f.state,f.context),f.state=f.__s;while(f.__d&&++k<25);f.state=f.__s,f.getChildContext!=null&&(i=v(v({},i),f.getChildContext())),T&&!p&&f.getSnapshotBeforeUpdate!=null&&(b=f.getSnapshotBeforeUpdate(m,g)),A=d!=null&&d.type===S&&d.key==null?re(d.props.children):d,c=ee(e,_(A)?A:[A],n,r,i,a,o,s,c,l,u),f.base=n.__e,n.__u&=-161,f.__h.length&&s.push(f),x&&(f.__E=f.__=null)}catch(e){if(n.__v=null,l||o!=null)if(e.then){for(n.__u|=l?160:128;c&&c.nodeType==8&&c.nextSibling;)c=c.nextSibling;o[o.indexOf(c)]=null,n.__e=c}else{for(j=o.length;j--;)y(o[j]);F(n)}else n.__e=r.__e,n.__k=r.__k,e.then||F(n);t.__e(e,n,r)}else o==null&&n.__v==r.__v?(n.__k=r.__k,n.__e=r.__e):c=n.__e=ie(r.__e,n,r,i,a,o,s,l,u);return(d=t.diffed)&&d(n),128&n.__u?void 0:c}function F(e){e&&(e.__c&&(e.__c.__e=!0),e.__k&&e.__k.some(F))}function ne(e,n,r){for(var i=0;i<r.length;i++)I(r[i],r[++i],r[++i]);t.__c&&t.__c(n,e),e.some(function(n){try{e=n.__h,n.__h=[],e.some(function(e){e.call(n)})}catch(e){t.__e(e,n.__v)}})}function re(e){return typeof e!=`object`||!e||e.__b>0?e:_(e)?e.map(re):v({},e)}function ie(n,r,i,a,o,s,c,l,u){var d,f,p,h,g,v,b,x=i.props||m,S=r.props,C=r.type;if(C==`svg`?o=`http://www.w3.org/2000/svg`:C==`math`?o=`http://www.w3.org/1998/Math/MathML`:o||=`http://www.w3.org/1999/xhtml`,s!=null){for(d=0;d<s.length;d++)if((g=s[d])&&`setAttribute`in g==!!C&&(C?g.localName==C:g.nodeType==3)){n=g,s[d]=null;break}}if(n==null){if(C==null)return document.createTextNode(S);n=document.createElementNS(o,C,S.is&&S),l&&=(t.__m&&t.__m(r,s),!1),s=null}if(C==null)x===S||l&&n.data==S||(n.data=S);else{if(s&&=e.call(n.childNodes),!l&&s!=null)for(x={},d=0;d<n.attributes.length;d++)x[(g=n.attributes[d]).name]=g.value;for(d in x)g=x[d],d==`dangerouslySetInnerHTML`?p=g:d==`children`||d in S||d==`value`&&`defaultValue`in S||d==`checked`&&`defaultChecked`in S||N(n,d,null,g,o);for(d in S)g=S[d],d==`children`?h=g:d==`dangerouslySetInnerHTML`?f=g:d==`value`?v=g:d==`checked`?b=g:l&&typeof g!=`function`||x[d]===g||N(n,d,g,x[d],o);if(f)l||p&&(f.__html==p.__html||f.__html==n.innerHTML)||(n.innerHTML=f.__html),r.__k=[];else if(p&&(n.innerHTML=``),ee(r.type==`template`?n.content:n,_(h)?h:[h],r,i,a,C==`foreignObject`?`http://www.w3.org/1999/xhtml`:o,s,c,s?s[0]:i.__k&&w(i,0),l,u),s!=null)for(d=s.length;d--;)y(s[d]);l||(d=`value`,C==`progress`&&v==null?n.removeAttribute(`value`):v!=null&&(v!==n[d]||C==`progress`&&!v||C==`option`&&v!=x[d])&&N(n,d,v,x[d],o),d=`checked`,b!=null&&b!=n[d]&&N(n,d,b,x[d],o))}return n}function I(e,n,r){try{if(typeof e==`function`){var i=typeof e.__u==`function`;i&&e.__u(),i&&n==null||(e.__u=e(n))}else e.current=n}catch(e){t.__e(e,r)}}function L(e,n,r){var i,a;if(t.unmount&&t.unmount(e),(i=e.ref)&&(i.current&&i.current!=e.__e||I(i,null,n)),(i=e.__c)!=null){if(i.componentWillUnmount)try{i.componentWillUnmount()}catch(e){t.__e(e,n)}i.base=i.__P=null}if(i=e.__k)for(a=0;a<i.length;a++)i[a]&&L(i[a],n,r||typeof e.type!=`function`);r||y(e.__e),e.__c=e.__=e.__e=void 0}function ae(e,t,n){return this.constructor(e,n)}function oe(n,r,i){var a,o,s,c;r==document&&(r=document.documentElement),t.__&&t.__(n,r),o=(a=typeof i==`function`)?null:i&&i.__k||r.__k,s=[],c=[],P(r,n=(!a&&i||r).__k=b(S,null,[n]),o||m,m,r.namespaceURI,!a&&i?[i]:o?null:r.firstChild?e.call(r.childNodes):null,s,!a&&i?i:o?o.__e:r.firstChild,a,c),ne(s,n,c)}e=h.slice,t={__e:function(e,t,n,r){for(var i,a,o;t=t.__;)if((i=t.__c)&&!i.__)try{if((a=i.constructor)&&a.getDerivedStateFromError!=null&&(i.setState(a.getDerivedStateFromError(e)),o=i.__d),i.componentDidCatch!=null&&(i.componentDidCatch(e,r||{}),o=i.__d),o)return i.__E=i}catch(t){e=t}throw e}},n=0,C.prototype.setState=function(e,t){var n=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=v({},this.state);typeof e==`function`&&(e=e(v({},n),this.props)),e&&v(n,e),e!=null&&this.__v&&(t&&this._sb.push(t),D(this))},C.prototype.forceUpdate=function(e){this.__v&&(this.__e=!0,e&&this.__h.push(e),D(this))},C.prototype.render=S,r=[],a=typeof Promise==`function`?Promise.prototype.then.bind(Promise.resolve()):setTimeout,o=function(e,t){return e.__v.__b-t.__v.__b},O.__r=0,s=Math.random().toString(8),c=`__d`+s,l=`__a`+s,u=/(PointerCapture)$|Capture$/i,d=0,f=te(!1),p=te(!0);var R,z,B,V,H=0,U=[],W=t,G=W.__b,K=W.__r,q=W.diffed,se=W.__c,ce=W.unmount,le=W.__;function ue(e,t){W.__h&&W.__h(z,e,H||t),H=0;var n=z.__H||={__:[],__h:[]};return e>=n.__.length&&n.__.push({}),n.__[e]}function J(e){return H=1,de(ge,e)}function de(e,t,n){var r=ue(R++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):ge(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);t!==n&&(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=z,!z.__f)){var i=function(e,t,n){if(!r.__c.__H)return!0;var i=r.__c.__H.__.filter(function(e){return e.__c});if(i.every(function(e){return!e.__N}))return!a||a.call(this,e,t,n);var o=r.__c.props!==e;return i.some(function(e){if(e.__N){var t=e.__[0];e.__=e.__N,e.__N=void 0,t!==e.__[0]&&(o=!0)}}),a&&a.call(this,e,t,n)||o};z.__f=!0;var a=z.shouldComponentUpdate,o=z.componentWillUpdate;z.componentWillUpdate=function(e,t,n){if(this.__e){var r=a;a=void 0,i(e,t,n),a=r}o&&o.call(this,e,t,n)},z.shouldComponentUpdate=i}return r.__N||r.__}function Y(e,t){var n=ue(R++,7);return he(n.__H,t)&&(n.__=e(),n.__H=t,n.__h=e),n.__}function fe(){for(var e;e=U.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(X),t.__h.some(Z),t.__h=[]}catch(n){t.__h=[],W.__e(n,e.__v)}}}W.__b=function(e){z=null,G&&G(e)},W.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),le&&le(e,t)},W.__r=function(e){K&&K(e),R=0;var t=(z=e.__c).__H;t&&(B===z?(t.__h=[],z.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})):(t.__h.some(X),t.__h.some(Z),t.__h=[],R=0)),B=z},W.diffed=function(e){q&&q(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(U.push(t)!==1&&V===W.requestAnimationFrame||((V=W.requestAnimationFrame)||me)(fe)),t.__H.__.some(function(e){e.u&&(e.__H=e.u),e.u=void 0})),B=z=null},W.__c=function(e,t){t.some(function(e){try{e.__h.some(X),e.__h=e.__h.filter(function(e){return!e.__||Z(e)})}catch(n){t.some(function(e){e.__h&&=[]}),t=[],W.__e(n,e.__v)}}),se&&se(e,t)},W.unmount=function(e){ce&&ce(e);var t,n=e.__c;n&&n.__H&&(n.__H.__.some(function(e){try{X(e)}catch(e){t=e}}),n.__H=void 0,t&&W.__e(t,n.__v))};var pe=typeof requestAnimationFrame==`function`;function me(e){var t,n=function(){clearTimeout(r),pe&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);pe&&(t=requestAnimationFrame(n))}function X(e){var t=z,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),z=t}function Z(e){var t=z;e.__c=e.__(),z=t}function he(e,t){return!e||e.length!==t.length||t.some(function(t,n){return t!==e[n]})}function ge(e,t){return typeof t==`function`?t(e):t}var _e=0;Array.isArray;function Q(e,n,r,i,a,o){n||={};var s,c,l=n;if(`ref`in l)for(c in l={},n)c==`ref`?s=n[c]:l[c]=n[c];var u={type:e,props:l,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--_e,__i:-1,__u:0,__source:a,__self:o};if(typeof e==`function`&&(s=e.defaultProps))for(c in s)l[c]===void 0&&(l[c]=s[c]);return t.vnode&&t.vnode(u),u}function ve({value:e,onInput:t,placeholder:n,id:r}){return Q(`div`,{class:`search-bar`,children:[Q(`label`,{class:`search-label`,htmlFor:r,children:`Filter`}),Q(`input`,{id:r,class:`search-input`,type:`search`,value:e,placeholder:n||`Type to filter rows...`,autoComplete:`off`,spellCheck:!1,onInput:e=>t(e.currentTarget.value)})]})}function $({id:e,caption:t,columns:n,items:r,emptyMessage:i}){let[a,o]=J(``),[s,c]=J({key:null,direction:`asc`}),l=Y(()=>n.map(e=>({...e,searchValue:ye(e)})),[n]),u=Y(()=>{if(!a)return r;let e=a.toLowerCase();return r.filter(t=>l.some(n=>{let r=n.searchValue(t);return r&&r.toLowerCase().includes(e)}))},[r,a,l]),d=Y(()=>{if(!s.key)return u;let e=n.find(e=>e.key===s.key);if(!e)return u;let t=e.sortValue||ye(e),r=s.direction===`desc`?-1:1;return[...u].sort((e,n)=>{let i=t(e),a=t(n);return i===a?0:i==null?1:a==null?-1:i>a?r:-r})},[u,s,n]),f=(e,t)=>{t&&(s.key===e?s.direction===`asc`?c({key:e,direction:`desc`}):c({key:null,direction:`asc`}):c({key:e,direction:`asc`}))};return Q(`div`,{class:`report-table`,children:[Q(ve,{id:`${e}-search`,value:a,onInput:o,placeholder:`Filter ${t.toLowerCase()}...`}),Q(`p`,{class:`report-table__counts`,"aria-live":`polite`,children:[`Showing `,d.length,` of `,r.length]}),Q(`table`,{class:`data-table`,"aria-describedby":`${e}-search`,children:[Q(`caption`,{class:`visually-hidden`,children:t}),Q(`thead`,{children:Q(`tr`,{children:n.map(e=>{let t=s.key===e.key,n=t?s.direction:`none`;return Q(`th`,{scope:`col`,class:`${e.className||``} ${e.sortable?`is-sortable`:``}`.trim(),"aria-sort":t?s.direction===`asc`?`ascending`:`descending`:`none`,children:e.sortable?Q(`button`,{type:`button`,class:`sort-button`,onClick:()=>f(e.key,e.sortable),"data-sort":n,children:[e.label,Q(`span`,{class:`sort-indicator`,"aria-hidden":`true`,children:t?s.direction===`asc`?`▲`:`▼`:`↕`})]}):e.label},e.key)})})}),Q(`tbody`,{children:[d.length===0&&Q(`tr`,{children:Q(`td`,{class:`report-table__empty`,colSpan:n.length,children:i||`No rows to display.`})}),d.map((e,t)=>Q(`tr`,{children:n.map(t=>t.render(e))},e.id||e.example_id||e.file_name||t))]})]})]})}function ye(e){return e.searchValue?e.searchValue:t=>{let n=t[e.key];return n==null?``:String(n)}}var be={passed:`status-passed`,failed:`status-failed`,pending:`status-pending`,skipped:`status-skipped`,flaky:`status-flaky`,interrupted:`status-failed`};function xe(e){return be[e]||`status-unknown`}function Se(e){let t=(e.execution_result||{}).run_time;return typeof t==`number`?t<.001?`${(t*1e6).toFixed(0)} \u00B5s`:t<1?`${(t*1e3).toFixed(1)} ms`:`${t.toFixed(3)} s`:``}var Ce=[{key:`description`,label:`Description`,sortable:!0,render:e=>Q(`td`,{class:`cell-description`,children:e.description||``}),searchValue:e=>`${e.description||``} ${e.id||``}`},{key:`location`,label:`Location`,sortable:!0,render:e=>Q(`td`,{class:`cell-location`,children:Q(`code`,{children:e.location||``})}),searchValue:e=>e.location||``},{key:`status`,label:`Status`,sortable:!0,render:e=>Q(`td`,{class:`cell-status`,children:Q(`span`,{class:`badge ${xe(e.status)}`,children:e.status||`unknown`})}),searchValue:e=>e.status||``},{key:`run_reason`,label:`Run reason`,sortable:!0,render:e=>Q(`td`,{class:`cell-reason`,children:e.run_reason||``}),searchValue:e=>e.run_reason||``},{key:`duration`,label:`Duration`,sortable:!0,className:`is-numeric`,render:e=>Q(`td`,{class:`cell-duration is-numeric`,children:Se(e)}),sortValue:e=>{let t=e.execution_result||{};return typeof t.run_time==`number`?t.run_time:-1},searchValue:e=>Se(e)}];function we({items:e}){return Q($,{id:`all-examples`,caption:`All examples`,columns:Ce,items:e,emptyMessage:`No examples tracked in this run.`})}function Te(e){let t=[];return e.forEach(e=>{(e.entries||[]).forEach((n,r)=>{t.push({id:`${e.id}-${r}`,groupId:e.id,count:e.count,description:n.description||``,location:n.location||``})})}),t}var Ee=[{key:`groupId`,label:`Example ID`,sortable:!0,render:e=>Q(`td`,{class:`cell-id`,children:Q(`code`,{children:e.groupId})}),searchValue:e=>e.groupId||``},{key:`count`,label:`Occurrences`,sortable:!0,className:`is-numeric`,render:e=>Q(`td`,{class:`cell-count is-numeric`,children:e.count}),sortValue:e=>e.count},{key:`description`,label:`Description`,sortable:!0,render:e=>Q(`td`,{class:`cell-description`,children:e.description})},{key:`location`,label:`Location`,sortable:!0,render:e=>Q(`td`,{class:`cell-location`,children:Q(`code`,{children:e.location})})}];function De({items:e}){return Q($,{id:`duplicate-examples`,caption:`Duplicate examples`,columns:Ee,items:Te(e||[]),emptyMessage:`No duplicate examples detected.`})}var Oe=[{key:`id`,label:`Example ID`,sortable:!0,render:e=>Q(`td`,{class:`cell-id`,children:Q(`code`,{children:e.id})})},{key:`description`,label:`Description`,sortable:!0,render:e=>Q(`td`,{class:`cell-description`,children:e.description||``})},{key:`location`,label:`Location`,sortable:!0,render:e=>Q(`td`,{class:`cell-location`,children:Q(`code`,{children:e.location||``})})}];function ke({items:e}){return Q($,{id:`flaky-examples`,caption:`Flaky examples`,columns:Oe,items:e,emptyMessage:`No flaky examples detected.`})}var Ae=[{key:`example_id`,label:`Example ID`,sortable:!0,render:e=>Q(`td`,{class:`cell-id`,children:Q(`code`,{children:e.example_id})})},{key:`files_count`,label:`Files`,sortable:!0,className:`is-numeric`,render:e=>Q(`td`,{class:`cell-count is-numeric`,children:(e.files||[]).length}),sortValue:e=>(e.files||[]).length,searchValue:e=>String((e.files||[]).length)},{key:`env_keys_count`,label:`Env keys`,sortable:!0,className:`is-numeric`,render:e=>Q(`td`,{class:`cell-count is-numeric`,children:(e.env_keys||[]).length}),sortValue:e=>(e.env_keys||[]).length,searchValue:e=>(e.env_keys||[]).join(` `)},{key:`files`,label:`Dependencies`,sortable:!1,render:e=>Q(`td`,{class:`cell-deps`,children:[(e.files||[]).length>0&&Q(`details`,{class:`deps-details`,children:[Q(`summary`,{children:[(e.files||[]).length,` files`]}),Q(`ul`,{class:`deps-list`,children:(e.files||[]).map(e=>Q(`li`,{children:Q(`code`,{children:e})},e))})]}),(e.env_keys||[]).length>0&&Q(`details`,{class:`deps-details deps-env`,children:[Q(`summary`,{children:[(e.env_keys||[]).length,` env keys`]}),Q(`ul`,{class:`deps-list`,children:(e.env_keys||[]).map(e=>Q(`li`,{children:Q(`code`,{children:e})},e))})]})]}),searchValue:e=>[...e.files||[],...e.env_keys||[]].join(` `)}];function je({items:e}){return Q($,{id:`examples-dependency`,caption:`Examples dependency`,columns:Ae,items:e,emptyMessage:`No dependencies tracked.`})}var Me=[{key:`file_name`,label:`File`,sortable:!0,render:e=>Q(`td`,{class:`cell-file`,children:Q(`code`,{children:e.file_name})})},{key:`example_count`,label:`Examples`,sortable:!0,className:`is-numeric`,render:e=>Q(`td`,{class:`cell-count is-numeric`,children:e.example_count||0}),sortValue:e=>e.example_count||0},{key:`spec_file_count`,label:`Spec files`,sortable:!0,className:`is-numeric`,render:e=>Q(`td`,{class:`cell-count is-numeric`,children:Object.keys(e.spec_files||{}).length}),sortValue:e=>Object.keys(e.spec_files||{}).length,searchValue:e=>Object.keys(e.spec_files||{}).join(` `)},{key:`spec_files`,label:`Dependent spec files`,sortable:!1,render:e=>{let t=Object.entries(e.spec_files||{});return t.length===0?Q(`td`,{class:`cell-deps`}):Q(`td`,{class:`cell-deps`,children:Q(`details`,{class:`deps-details`,children:[Q(`summary`,{children:[t.length,` spec files`]}),Q(`ul`,{class:`deps-list`,children:t.map(([e,t])=>Q(`li`,{children:[Q(`code`,{children:e}),Q(`span`,{class:`dep-count`,children:[`×`,t]})]},e))})]})})},searchValue:e=>Object.keys(e.spec_files||{}).join(` `)}];function Ne({items:e}){return Q($,{id:`files-dependency`,caption:`Files dependency`,columns:Me,items:e,emptyMessage:`No file dependencies tracked.`})}function Pe(e){let t=e&&e.reports||{},n=[{id:`all_examples`,label:`All Examples`,count:(t.all_examples||[]).length,render:()=>Q(we,{items:t.all_examples||[]})}];return(t.duplicate_examples||[]).length>0&&n.push({id:`duplicate_examples`,label:`Duplicate Examples`,count:t.duplicate_examples.length,render:()=>Q(De,{items:t.duplicate_examples})}),(t.flaky_examples||[]).length>0&&n.push({id:`flaky_examples`,label:`Flaky Examples`,count:t.flaky_examples.length,render:()=>Q(ke,{items:t.flaky_examples})}),n.push({id:`examples_dependency`,label:`Examples Dependency`,count:(t.examples_dependency||[]).length,render:()=>Q(je,{items:t.examples_dependency||[]})}),n.push({id:`files_dependency`,label:`Files Dependency`,count:(t.files_dependency||[]).length,render:()=>Q(Ne,{items:t.files_dependency||[]})}),n}function Fe({payload:e}){let t=Y(()=>Pe(e),[e]),[n,r]=J(t[0]?t[0].id:null),i=e&&e.summary||{},a=e&&e.generated_at||``,o=e&&e.run_id||``,s=t.find(e=>e.id===n)||t[0];return Q(`div`,{class:`report-root`,children:[Q(`header`,{class:`report-header`,children:[Q(`h1`,{children:`RSpec Tracer Report`}),Q(`dl`,{class:`summary`,children:[Q(`div`,{class:`summary-item`,children:[Q(`dt`,{children:`Total`}),Q(`dd`,{children:i.total_examples??0})]}),Q(`div`,{class:`summary-item summary-passed`,children:[Q(`dt`,{children:`Passed`}),Q(`dd`,{children:i.passed_examples??0})]}),Q(`div`,{class:`summary-item summary-failed`,children:[Q(`dt`,{children:`Failed`}),Q(`dd`,{children:i.failed_examples??0})]}),Q(`div`,{class:`summary-item summary-pending`,children:[Q(`dt`,{children:`Pending`}),Q(`dd`,{children:i.pending_examples??0})]}),Q(`div`,{class:`summary-item summary-skipped`,children:[Q(`dt`,{children:`Skipped`}),Q(`dd`,{children:i.skipped_examples??0})]}),Q(`div`,{class:`summary-item`,children:[Q(`dt`,{children:`Flaky`}),Q(`dd`,{children:i.flaky_examples??0})]})]}),Q(`p`,{class:`report-meta`,children:[Q(`span`,{children:[`Run `,Q(`code`,{children:o})]}),Q(`span`,{children:[`Generated `,a]})]})]}),Q(`nav`,{class:`tab-bar`,role:`tablist`,"aria-label":`Report sections`,children:t.map(e=>Q(`button`,{type:`button`,role:`tab`,class:`tab-button${e.id===(s&&s.id)?` is-active`:``}`,"aria-selected":e.id===(s&&s.id),"aria-controls":`panel-${e.id}`,id:`tab-${e.id}`,onClick:()=>r(e.id),children:[e.label,Q(`span`,{class:`tab-count`,children:e.count})]},e.id))}),s&&Q(`section`,{class:`report-panel`,role:`tabpanel`,id:`panel-${s.id}`,"aria-labelledby":`tab-${s.id}`,children:s.render()})]})}function Ie(){let e=document.getElementById(`app`);if(!e)return;let t=document.getElementById(`report-data`);if(!t)return;let n;try{n=JSON.parse(t.textContent||`{}`)}catch(e){console.error(`rspec-tracer: failed to parse embedded report data`,e);return}oe(Q(Fe,{payload:n}),e),e.setAttribute(`data-hydrate`,`ready`);let r=document.getElementById(`fallback`);r&&r.remove()}document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,Ie):Ie();
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="generator" content="rspec-tracer" />
7
+ <title>RSpec Tracer Report</title>
8
+ <script type="module" crossorigin src="./assets/index.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index.css">
10
+ </head>
11
+ <body>
12
+ <noscript>
13
+ <div class="noscript-banner">
14
+ JavaScript is disabled. Interactive search and sort are unavailable; the static tables below
15
+ contain the full run data.
16
+ </div>
17
+ </noscript>
18
+ <div id="app" data-hydrate="pending"></div>
19
+ <!-- RSPEC_TRACER_FALLBACK -->
20
+ <script id="report-data" type="application/json">
21
+ {}
22
+ </script>
23
+ </body>
24
+ </html>