browsable 0.1.0 → 0.2.0.pre.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/CHANGELOG.md +34 -0
- data/README.md +340 -147
- data/bin/benchmark-asset-resolution +91 -0
- data/lib/browsable/asset_resolver.rb +200 -0
- data/lib/browsable/audit_log.rb +97 -0
- data/lib/browsable/cli.rb +24 -0
- data/lib/browsable/drivers/minitest.rb +100 -0
- data/lib/browsable/drivers/rspec.rb +117 -0
- data/lib/browsable/html_extractor.rb +114 -0
- data/lib/browsable/middleware.rb +124 -0
- data/lib/browsable/minitest.rb +25 -0
- data/lib/browsable/policy.rb +63 -0
- data/lib/browsable/policy_resolver.rb +153 -0
- data/lib/browsable/replay.rb +115 -0
- data/lib/browsable/rspec.rb +28 -0
- data/lib/browsable/test_report.rb +329 -0
- data/lib/browsable/version.rb +1 -1
- data/lib/browsable.rb +8 -4
- metadata +57 -2
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module Browsable
|
|
8
|
+
# The end-of-suite reporting layer for runtime mode.
|
|
9
|
+
#
|
|
10
|
+
# TestReport reads the AuditLog, invokes the v0.1 analyzers **in batch** —
|
|
11
|
+
# one stylelint call, one eslint call regardless of suite size — then groups
|
|
12
|
+
# the resulting findings by endpoint and renders them. It is the only place
|
|
13
|
+
# in runtime mode where a subprocess is spawned.
|
|
14
|
+
#
|
|
15
|
+
# The per-endpoint policy is applied when grouping findings: a CSS feature
|
|
16
|
+
# that flagged the batch target is attributed to every endpoint that loaded
|
|
17
|
+
# the file. HTML findings, which carry exact required versions, can be
|
|
18
|
+
# re-evaluated against each endpoint's specific policy (TODO: v0.2.1).
|
|
19
|
+
class TestReport
|
|
20
|
+
# An analyzer outcome grouped by endpoint, used for rendering.
|
|
21
|
+
EndpointReport = Data.define(:endpoint, :policy, :findings)
|
|
22
|
+
|
|
23
|
+
attr_reader :audit_log, :config, :root
|
|
24
|
+
|
|
25
|
+
def initialize(audit_log: Browsable.audit_log, config: nil, root: nil)
|
|
26
|
+
@audit_log = audit_log
|
|
27
|
+
@root = root || (defined?(Rails) && Rails.application ? Rails.root.to_s : Dir.pwd)
|
|
28
|
+
@config = config || Config.load(root: @root)
|
|
29
|
+
@analyzed = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Findings (Array<Finding>) discovered across the whole suite.
|
|
33
|
+
def findings
|
|
34
|
+
analyze_if_needed
|
|
35
|
+
@findings
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Findings grouped per observed endpoint, sorted by endpoint name.
|
|
39
|
+
def endpoint_reports
|
|
40
|
+
analyze_if_needed
|
|
41
|
+
@endpoint_reports
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Asset paths that could not be resolved against the host app — surfaced as
|
|
45
|
+
# skipped entries so the user understands what the audit could not cover.
|
|
46
|
+
def skipped_assets
|
|
47
|
+
analyze_if_needed
|
|
48
|
+
@skipped_assets
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def errors?
|
|
52
|
+
analyze_if_needed
|
|
53
|
+
@findings.any?(&:error?)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def warnings?
|
|
57
|
+
analyze_if_needed
|
|
58
|
+
@findings.any?(&:warning?)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Build a Report whose shape v0.1's formatters can consume, so runtime
|
|
62
|
+
# output can use the same Human/Json/Github renderers as the CLI.
|
|
63
|
+
def to_report
|
|
64
|
+
analyze_if_needed
|
|
65
|
+
Report.new(
|
|
66
|
+
findings: @findings,
|
|
67
|
+
skips: @skips,
|
|
68
|
+
notes: @notes,
|
|
69
|
+
policies: PolicyScanner.call(root),
|
|
70
|
+
target: batch_target,
|
|
71
|
+
root: root,
|
|
72
|
+
config_file: config.config_file
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Render the report as a string in the requested format.
|
|
77
|
+
def render(format: :human)
|
|
78
|
+
formatter_for(format).new(to_report).render
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_json(*_args)
|
|
82
|
+
JSON.pretty_generate(to_report.as_json.merge(
|
|
83
|
+
endpoints: endpoint_reports.map { |er| endpoint_as_json(er) },
|
|
84
|
+
skipped_assets: skipped_assets
|
|
85
|
+
))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Convenience: exit non-zero when any error finding exists. Drivers call
|
|
89
|
+
# this from their after(:suite) hook when configured to fail on errors.
|
|
90
|
+
def fail_suite_if_errors!(fail_on: :error)
|
|
91
|
+
should_fail =
|
|
92
|
+
case fail_on.to_s
|
|
93
|
+
when "warning" then errors? || warnings?
|
|
94
|
+
when "error" then errors?
|
|
95
|
+
else false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Kernel.exit(1) if should_fail
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# ---- batch analysis ------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def analyze_if_needed
|
|
106
|
+
return if @analyzed
|
|
107
|
+
|
|
108
|
+
@analyzed = true
|
|
109
|
+
@findings = []
|
|
110
|
+
@skips = []
|
|
111
|
+
@notes = []
|
|
112
|
+
@skipped_assets = []
|
|
113
|
+
@endpoint_reports = []
|
|
114
|
+
|
|
115
|
+
return if audit_log.empty?
|
|
116
|
+
|
|
117
|
+
target = batch_target
|
|
118
|
+
run_css_batch(target)
|
|
119
|
+
run_js_batch(target)
|
|
120
|
+
run_html_per_entry
|
|
121
|
+
|
|
122
|
+
@endpoint_reports = group_by_endpoint
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The target the batch invocations are configured against: the most-strict
|
|
126
|
+
# union of every recorded policy, so the analyzer flags any feature that
|
|
127
|
+
# could matter for any endpoint. Per-endpoint precision is re-asserted
|
|
128
|
+
# later when severities are reassigned.
|
|
129
|
+
def batch_target
|
|
130
|
+
@batch_target ||= compute_batch_target
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def compute_batch_target
|
|
134
|
+
targets = audit_log.entries.map { |entry| entry.policy.target }.uniq { |t| t.browsers }
|
|
135
|
+
return config.target if targets.empty?
|
|
136
|
+
|
|
137
|
+
# Minimum version per browser across all observed targets — i.e. the
|
|
138
|
+
# browser-support floor every endpoint shares.
|
|
139
|
+
union = {}
|
|
140
|
+
targets.each do |t|
|
|
141
|
+
t.browsers.each do |browser, version|
|
|
142
|
+
existing = union[browser]
|
|
143
|
+
union[browser] = version if existing.nil? || gem_version(version) < gem_version(existing)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Target.new("runtime-union", resolved: union)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def run_css_batch(target)
|
|
151
|
+
paths_on_disk = audit_log.asset_path_universe.select { |p| css?(p) && File.file?(p) }
|
|
152
|
+
tmp_files = write_inline_blocks(:css)
|
|
153
|
+
all_files = paths_on_disk.to_a + tmp_files
|
|
154
|
+
|
|
155
|
+
track_unresolved(:css)
|
|
156
|
+
return if all_files.empty?
|
|
157
|
+
return record_skip(:css, "stylelint not found — run `browsable doctor`") unless available?(:css)
|
|
158
|
+
|
|
159
|
+
analyzer = Analyzers::CSS.new(target: target, config: config)
|
|
160
|
+
collected = safe_analyze(:css, analyzer, all_files)
|
|
161
|
+
remap_tmp_findings(collected, tmp_files, :css)
|
|
162
|
+
@findings.concat(collected)
|
|
163
|
+
ensure
|
|
164
|
+
cleanup(tmp_files) if tmp_files
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def run_js_batch(target)
|
|
168
|
+
paths_on_disk = audit_log.asset_path_universe.select { |p| js?(p) && File.file?(p) }
|
|
169
|
+
tmp_files = write_inline_blocks(:js)
|
|
170
|
+
all_files = paths_on_disk.to_a + tmp_files
|
|
171
|
+
|
|
172
|
+
track_unresolved(:js)
|
|
173
|
+
return if all_files.empty?
|
|
174
|
+
return record_skip(:js, "eslint not found — run `browsable doctor`") unless available?(:js)
|
|
175
|
+
|
|
176
|
+
analyzer = Analyzers::Javascript.new(target: target, config: config)
|
|
177
|
+
collected = safe_analyze(:js, analyzer, all_files)
|
|
178
|
+
remap_tmp_findings(collected, tmp_files, :js)
|
|
179
|
+
@findings.concat(collected)
|
|
180
|
+
ensure
|
|
181
|
+
cleanup(tmp_files) if tmp_files
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def run_html_per_entry
|
|
185
|
+
audit_log.entries.each do |entry|
|
|
186
|
+
next if entry.html.empty?
|
|
187
|
+
|
|
188
|
+
analyzer = Analyzers::HTML.new(target: entry.policy.target, config: config)
|
|
189
|
+
results = analyzer.analyze_source(entry.html, file: synthetic_path_for(entry))
|
|
190
|
+
@findings.concat(results)
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
record_skip(:html, "HTML analysis failed for #{entry.endpoint}: #{e.message}")
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# ---- attribution + grouping ---------------------------------------------
|
|
197
|
+
|
|
198
|
+
# Build per-endpoint groupings. For each endpoint, gather:
|
|
199
|
+
# - findings on HTML it produced (via synthetic path)
|
|
200
|
+
# - findings on assets it loaded (looked up via AuditLog#entries_loading)
|
|
201
|
+
def group_by_endpoint
|
|
202
|
+
by_endpoint = Hash.new { |h, k| h[k] = [] }
|
|
203
|
+
endpoint_policies = {}
|
|
204
|
+
|
|
205
|
+
audit_log.entries.each do |entry|
|
|
206
|
+
endpoint_policies[entry.endpoint] ||= entry.policy
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
@findings.each do |finding|
|
|
210
|
+
owners = endpoints_for(finding)
|
|
211
|
+
owners.each do |endpoint|
|
|
212
|
+
by_endpoint[endpoint] << finding
|
|
213
|
+
endpoint_policies[endpoint] ||= policy_for(endpoint)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
by_endpoint.sort.map do |endpoint, list|
|
|
218
|
+
EndpointReport.new(
|
|
219
|
+
endpoint: endpoint,
|
|
220
|
+
policy: endpoint_policies[endpoint],
|
|
221
|
+
findings: list.uniq
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def endpoints_for(finding)
|
|
227
|
+
file = finding.file
|
|
228
|
+
synthetic = synthetic_prefix
|
|
229
|
+
return [file.sub(synthetic, "")] if file.start_with?(synthetic)
|
|
230
|
+
|
|
231
|
+
audit_log.entries_loading(file).map(&:endpoint).uniq
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def policy_for(endpoint)
|
|
235
|
+
entry = audit_log.entries.find { |e| e.endpoint == endpoint }
|
|
236
|
+
entry&.policy
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# ---- helpers ------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def write_inline_blocks(kind)
|
|
242
|
+
blocks = audit_log.entries.flat_map(&:inline_blocks).select { |b| b.kind == kind }
|
|
243
|
+
return [] if blocks.empty?
|
|
244
|
+
|
|
245
|
+
dir = (@inline_dir ||= Dir.mktmpdir("browsable-inline"))
|
|
246
|
+
ext = kind == :css ? "css" : "js"
|
|
247
|
+
blocks.uniq(&:content).each_with_index.map do |block, idx|
|
|
248
|
+
path = File.join(dir, "inline-#{idx}.#{ext}")
|
|
249
|
+
File.write(path, block.content)
|
|
250
|
+
path
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def cleanup(paths)
|
|
255
|
+
paths.each { |p| File.delete(p) if File.file?(p) }
|
|
256
|
+
rescue StandardError
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Rewrite a finding's file from the tmpdir path to something users will
|
|
261
|
+
# recognize ("(inline <style> block)"), so reports never leak temp paths.
|
|
262
|
+
def remap_tmp_findings(findings, tmp_files, kind)
|
|
263
|
+
tmp_set = tmp_files.to_set
|
|
264
|
+
label = kind == :css ? "(inline <style> block)" : "(inline <script> block)"
|
|
265
|
+
findings.map! do |f|
|
|
266
|
+
next f unless tmp_set.include?(f.file)
|
|
267
|
+
|
|
268
|
+
Finding.new(**f.to_h.merge(file: label))
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def track_unresolved(kind)
|
|
273
|
+
audit_log.entries.each do |entry|
|
|
274
|
+
entry.asset_paths.each do |ref|
|
|
275
|
+
next unless ref.kind == kind
|
|
276
|
+
next unless ref.resolved_path.nil?
|
|
277
|
+
|
|
278
|
+
@skipped_assets << { url: ref.url, kind: ref.kind.to_s, endpoint: entry.endpoint }
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def safe_analyze(kind, analyzer, files)
|
|
284
|
+
analyzer.analyze(files)
|
|
285
|
+
rescue StandardError => e
|
|
286
|
+
record_skip(kind, "#{kind} analysis failed: #{e.message}")
|
|
287
|
+
[]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def record_skip(kind, reason)
|
|
291
|
+
@skips << Report::Skip.new(kind: kind, reason: reason)
|
|
292
|
+
[]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def available?(kind)
|
|
296
|
+
Doctor.new.available_kinds.include?(kind)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def css?(path) = %w[.css .scss].include?(File.extname(path).downcase)
|
|
300
|
+
def js?(path) = %w[.js .mjs].include?(File.extname(path).downcase)
|
|
301
|
+
|
|
302
|
+
SYNTHETIC_PREFIX = "[response] "
|
|
303
|
+
|
|
304
|
+
def synthetic_prefix = SYNTHETIC_PREFIX
|
|
305
|
+
def synthetic_path_for(entry) = "#{SYNTHETIC_PREFIX}#{entry.endpoint}"
|
|
306
|
+
|
|
307
|
+
def endpoint_as_json(report)
|
|
308
|
+
{
|
|
309
|
+
endpoint: report.endpoint,
|
|
310
|
+
policy: report.policy&.as_json,
|
|
311
|
+
findings: report.findings.map(&:as_json)
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def formatter_for(format)
|
|
316
|
+
case format.to_sym
|
|
317
|
+
when :json then Formatters::Json
|
|
318
|
+
when :github then Formatters::Github
|
|
319
|
+
else Formatters::Human
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def gem_version(value)
|
|
324
|
+
Gem::Version.new(value.to_s)
|
|
325
|
+
rescue ArgumentError
|
|
326
|
+
Gem::Version.new("0")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
data/lib/browsable/version.rb
CHANGED
data/lib/browsable.rb
CHANGED
|
@@ -30,15 +30,19 @@ end
|
|
|
30
30
|
|
|
31
31
|
Browsable.loader = Zeitwerk::Loader.for_gem
|
|
32
32
|
Browsable.loader.inflector.inflect(
|
|
33
|
-
"cli"
|
|
34
|
-
"css"
|
|
35
|
-
"erb"
|
|
36
|
-
"html"
|
|
33
|
+
"cli" => "CLI",
|
|
34
|
+
"css" => "CSS",
|
|
35
|
+
"erb" => "ERB",
|
|
36
|
+
"html" => "HTML",
|
|
37
|
+
"rspec" => "RSpec"
|
|
37
38
|
)
|
|
38
39
|
# These files intentionally do not define a constant matching their path.
|
|
39
40
|
Browsable.loader.ignore("#{__dir__}/browsable/version.rb")
|
|
40
41
|
Browsable.loader.ignore("#{__dir__}/browsable/rake_tasks.rb")
|
|
41
42
|
Browsable.loader.ignore("#{__dir__}/browsable/railtie.rb")
|
|
43
|
+
# Driver entry points: they require the gem and call into Drivers::X.install!.
|
|
44
|
+
Browsable.loader.ignore("#{__dir__}/browsable/rspec.rb")
|
|
45
|
+
Browsable.loader.ignore("#{__dir__}/browsable/minitest.rb")
|
|
42
46
|
# Rails generators are discovered and loaded by Rails itself, not Zeitwerk.
|
|
43
47
|
Browsable.loader.ignore("#{__dir__}/generators")
|
|
44
48
|
Browsable.loader.setup
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browsable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0.pre.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hood
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
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: concurrent-ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.2'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: herb
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +37,20 @@ dependencies:
|
|
|
23
37
|
- - ">="
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
39
|
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: nokogiri
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.13'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.13'
|
|
26
54
|
- !ruby/object:Gem::Dependency
|
|
27
55
|
name: pastel
|
|
28
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +79,20 @@ dependencies:
|
|
|
51
79
|
- - ">="
|
|
52
80
|
- !ruby/object:Gem::Version
|
|
53
81
|
version: '1.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rack
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '2.2'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '2.2'
|
|
54
96
|
- !ruby/object:Gem::Dependency
|
|
55
97
|
name: thor
|
|
56
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -121,6 +163,7 @@ extra_rdoc_files: []
|
|
|
121
163
|
files:
|
|
122
164
|
- CHANGELOG.md
|
|
123
165
|
- README.md
|
|
166
|
+
- bin/benchmark-asset-resolution
|
|
124
167
|
- bin/update-bcd-snapshot
|
|
125
168
|
- data/bcd-snapshot.json
|
|
126
169
|
- exe/browsable
|
|
@@ -130,18 +173,29 @@ files:
|
|
|
130
173
|
- lib/browsable/analyzers/erb.rb
|
|
131
174
|
- lib/browsable/analyzers/html.rb
|
|
132
175
|
- lib/browsable/analyzers/javascript.rb
|
|
176
|
+
- lib/browsable/asset_resolver.rb
|
|
177
|
+
- lib/browsable/audit_log.rb
|
|
133
178
|
- lib/browsable/cli.rb
|
|
134
179
|
- lib/browsable/config.rb
|
|
135
180
|
- lib/browsable/doctor.rb
|
|
181
|
+
- lib/browsable/drivers/minitest.rb
|
|
182
|
+
- lib/browsable/drivers/rspec.rb
|
|
136
183
|
- lib/browsable/finding.rb
|
|
137
184
|
- lib/browsable/formatters/github.rb
|
|
138
185
|
- lib/browsable/formatters/human.rb
|
|
139
186
|
- lib/browsable/formatters/json.rb
|
|
187
|
+
- lib/browsable/html_extractor.rb
|
|
188
|
+
- lib/browsable/middleware.rb
|
|
189
|
+
- lib/browsable/minitest.rb
|
|
190
|
+
- lib/browsable/policy.rb
|
|
140
191
|
- lib/browsable/policy_detector.rb
|
|
192
|
+
- lib/browsable/policy_resolver.rb
|
|
141
193
|
- lib/browsable/policy_scanner.rb
|
|
142
194
|
- lib/browsable/railtie.rb
|
|
143
195
|
- lib/browsable/rake_tasks.rb
|
|
196
|
+
- lib/browsable/replay.rb
|
|
144
197
|
- lib/browsable/report.rb
|
|
198
|
+
- lib/browsable/rspec.rb
|
|
145
199
|
- lib/browsable/sources/base.rb
|
|
146
200
|
- lib/browsable/sources/builds.rb
|
|
147
201
|
- lib/browsable/sources/importmap.rb
|
|
@@ -150,6 +204,7 @@ files:
|
|
|
150
204
|
- lib/browsable/sources/stylesheets.rb
|
|
151
205
|
- lib/browsable/sources/views.rb
|
|
152
206
|
- lib/browsable/target.rb
|
|
207
|
+
- lib/browsable/test_report.rb
|
|
153
208
|
- lib/browsable/version.rb
|
|
154
209
|
- lib/generators/browsable/install/install_generator.rb
|
|
155
210
|
- lib/generators/browsable/install/templates/browsable.yml.tt
|
|
@@ -179,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
179
234
|
- !ruby/object:Gem::Version
|
|
180
235
|
version: '0'
|
|
181
236
|
requirements: []
|
|
182
|
-
rubygems_version: 4.0.
|
|
237
|
+
rubygems_version: 4.0.0
|
|
183
238
|
specification_version: 4
|
|
184
239
|
summary: Rails-aware browser-compatibility audit for your frontend code.
|
|
185
240
|
test_files: []
|