stormbreaker 0.0.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 +7 -0
- data/.gitignore +6 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +13 -0
- data/lib/stormbreaker.rb +53 -0
- data/lib/stormbreaker/axe_helper.rb +61 -0
- data/lib/stormbreaker/axe_matcher_auditor.rb +14 -0
- data/lib/stormbreaker/axe_results_reporter.rb +13 -0
- data/lib/stormbreaker/axe_results_serializer.rb +32 -0
- data/lib/stormbreaker/axe_rspec_auditor.rb +17 -0
- data/lib/stormbreaker/axe_violation.rb +23 -0
- data/lib/stormbreaker/axe_violation_manager.rb +111 -0
- data/lib/stormbreaker/configuration.rb +42 -0
- data/lib/stormbreaker/erb_formatter.rb +51 -0
- data/lib/stormbreaker/tasks/results.rake +11 -0
- data/lib/stormbreaker/version.rb +5 -0
- data/lib/templates/results.erb +104 -0
- data/stormbreaker.gemspec +38 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd273780abddd41efbfc602152055da5cb92ed5211ac5bfdf0f9b9cbec84b3f7
|
4
|
+
data.tar.gz: a0d8c935d716fe359089341b91b6d0f8e2542128585248ff286790585ed64a06
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 29d34e7136a40cd9b81c8b0681fd2eeb0eb535da2bd2a986eb56da651afc1f2e3398c49cadd59560f5d02a14587f25d3b6c33a3ffc85e5cfb2988aff30548be4
|
7
|
+
data.tar.gz: 4dde21786a8e158c6b2851839977126bfd20e78a3213a8862a025bb759761de8c5752b5326658e9ca771e131ac6268468937121f432bf50f2e60c32ee8c1086f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2021 Instructure, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Stormbreaker
|
2
|
+
|
3
|
+
Stormbreaker automatically adds axe assertions to your ruby selenium tests on top of rspec assertions
|
4
|
+
while providing some additional functionality to sort, group, and display accessibility violations.
|
5
|
+
While it would be fairly easy to add the axe-rspec matchers to an existing suite, stormbreaker adds
|
6
|
+
simple on/off functionality so that you don't have to bloat your testing suite with a huge number of
|
7
|
+
axe assertions. Various configuration options exist to allow certain accessibility rules or severity
|
8
|
+
types (critical / serious / moderate / minor) to be enabled/disabled.
|
9
|
+
|
10
|
+
Rules can be found at https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
|
11
|
+
|
12
|
+
Stormbreaker provides extra value for mature testing suites where selenium tests exercise a majority
|
13
|
+
of an application's views.
|
14
|
+
|
15
|
+
### What does adding assertions on top of RSpec assertions mean?
|
16
|
+
|
17
|
+
```
|
18
|
+
expect(page.find_element('#important_title').text).to eq('Important Title')
|
19
|
+
```
|
20
|
+
|
21
|
+
*__Without Stormbreaker__*:
|
22
|
+
|
23
|
+
In the above example, selenium would retrieve the text of an element on a page and then check
|
24
|
+
its contents with RSpec.
|
25
|
+
|
26
|
+
*__With Stormbreaker__*:
|
27
|
+
|
28
|
+
In the above example, selenium will still retrieve the text of an element on a page and then check
|
29
|
+
its contents with RSpec, but it will first check to see if page is accessible according to the
|
30
|
+
configured rules.
|
31
|
+
|
32
|
+
An accessibility violation will not immediately cause a test to fail. Instead, a test will run either
|
33
|
+
until completion, or until a failed non-axe assertion. Any axe violations will be collected along the
|
34
|
+
way, ensuring that each test provides as much accessibility coverage as possible. Accessibility
|
35
|
+
violations will be reported on a detailed per-test basis and will be summarized for each suite.
|
36
|
+
|
37
|
+
In addition to violations being reported in the standard RSpec output, an html file utilizing client-side
|
38
|
+
javascript will also be produced to help sort violations by severity and violation type.
|
39
|
+
|
40
|
+
### Configuring Stormbreaker
|
41
|
+
|
42
|
+
In your spec_helper.rb add:
|
43
|
+
|
44
|
+
```
|
45
|
+
require 'stormbreaker'
|
46
|
+
Stormbreaker.install!
|
47
|
+
Stormbreaker.configure do |config|
|
48
|
+
# Driver configuration -- can be a lambda or an object. May need to be a lambda if your driver isnt
|
49
|
+
# available in your spec_helper.rb. This wont be referenced until an assertion is first run
|
50
|
+
config.driver = lambda { Driver.new }
|
51
|
+
|
52
|
+
# Axe config
|
53
|
+
config.rules = %i[wcag2a wcag2aa section508]
|
54
|
+
config.skip = %i[color-contrast duplicate-id]
|
55
|
+
config.enabled_severity_categories = %i[critical serious moderate minor]
|
56
|
+
|
57
|
+
# Standard results output
|
58
|
+
config.results_path = 'results.html'
|
59
|
+
|
60
|
+
# Serialization configuration
|
61
|
+
config.serialize_output = true
|
62
|
+
config.serialized_input_path = '.'
|
63
|
+
config.serialize_prefix = 'stormbreaker_results'
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
After adding stormbreaker to your Gemfile and installing it, then run `bundle exec rspec`
|
68
|
+
|
69
|
+
### Running tests in parallel
|
70
|
+
|
71
|
+
Stormbreaker's code is largely prepended onto RSpec's code, so it should not affect any parallelization.
|
72
|
+
However, if you'd like to generate an html that includes all of the different runs, stormbreaker provides
|
73
|
+
an option to dump each run's serialized results and then combine them into a single html file using a
|
74
|
+
rake task.
|
75
|
+
|
76
|
+
`bundle exec rake stormbreaker:combine_results['<path/to/#{serialize_prefix}*>']`
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
|
5
|
+
CLOBBER.include(
|
6
|
+
'coverage/',
|
7
|
+
'pkg/',
|
8
|
+
'stormbreaker-*.gem',
|
9
|
+
'Gemfile.lock'
|
10
|
+
)
|
11
|
+
|
12
|
+
path = File.expand_path(__dir__)
|
13
|
+
Dir.glob("#{path}/lib/stormbreaker/tasks/**/*.rake").each { |f| import f }
|
data/lib/stormbreaker.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'axe-selenium'
|
4
|
+
require 'axe-rspec'
|
5
|
+
require 'rspec/core/formatters/base_text_formatter'
|
6
|
+
|
7
|
+
require 'stormbreaker/configuration'
|
8
|
+
require 'stormbreaker/axe_helper'
|
9
|
+
require 'stormbreaker/axe_matcher_auditor'
|
10
|
+
require 'stormbreaker/axe_results_reporter'
|
11
|
+
require 'stormbreaker/axe_rspec_auditor'
|
12
|
+
require 'stormbreaker/axe_violation_manager'
|
13
|
+
require 'stormbreaker/axe_violation'
|
14
|
+
require 'stormbreaker/erb_formatter'
|
15
|
+
require 'stormbreaker/axe_results_serializer'
|
16
|
+
|
17
|
+
module Stormbreaker
|
18
|
+
autoload :VERSION, 'stormbreaker/version'
|
19
|
+
|
20
|
+
def self.install!
|
21
|
+
::RSpec::Expectations::ExpectationTarget.prepend AxeRSpecAuditor
|
22
|
+
::Axe::Matchers::BeAxeClean.prepend AxeMatcherAuditor
|
23
|
+
::RSpec::Core::Formatters::BaseTextFormatter.prepend AxeResultsReporter
|
24
|
+
configure_rspec_hooks
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.configure_rspec_hooks
|
28
|
+
RSpec.configure do |config|
|
29
|
+
config.before(:each) do
|
30
|
+
Stormbreaker::AxeHelper.post_test_cleanup
|
31
|
+
end
|
32
|
+
|
33
|
+
config.before(:suite) do
|
34
|
+
Stormbreaker::AxeHelper.post_total_cleanup
|
35
|
+
end
|
36
|
+
|
37
|
+
config.after(:each) do
|
38
|
+
unless Stormbreaker::AxeHelper.example_passed?
|
39
|
+
raise RSpec::Expectations::ExpectationNotMetError, Stormbreaker::AxeHelper.detailed_results
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
config.after(:suite) do
|
44
|
+
erb_writer = Stormbreaker::ErbFormatter.new(Stormbreaker::AxeHelper.manager.total_violations)
|
45
|
+
erb_writer.write_to_erb
|
46
|
+
|
47
|
+
if Stormbreaker.configuration.serialize_output
|
48
|
+
Stormbreaker::AxeResultsSerializer.serialize_results(Stormbreaker::AxeHelper.manager.total_violations)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ::Axe::Matchers::BeAxeClean.prepend AxeMatcherAuditor
|
4
|
+
module Stormbreaker
|
5
|
+
class AxeHelper
|
6
|
+
def self.assert_axe
|
7
|
+
@driver ||= Stormbreaker.configured_driver
|
8
|
+
raise 'Driver is not configured' unless @driver
|
9
|
+
|
10
|
+
axe_matcher = Axe::Matchers::BeAxeClean.new
|
11
|
+
axe_matcher.according_to configuration.rules
|
12
|
+
axe_matcher.skipping configuration.skip
|
13
|
+
|
14
|
+
# Always assert that driver's current page _is_ axe compliant
|
15
|
+
RSpec::Expectations::PositiveExpectationHandler.handle_matcher(@driver, axe_matcher)
|
16
|
+
|
17
|
+
call_stack = caller.select { |line| line =~ /selenium.*_spec\.rb/ }.first(5)
|
18
|
+
# only get /path/to/spec:line_number from call stack
|
19
|
+
call_stack.map! { |c| c.scan(%r{^[A-Za-z/_-]*\.rb:\d*}) }
|
20
|
+
|
21
|
+
violations = axe_matcher.audit([]).results.violations
|
22
|
+
violations.each do |v|
|
23
|
+
v.nodes.each do |node|
|
24
|
+
manager.add_failure(called_by: call_stack, violation: v, node: node)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.manager
|
30
|
+
@manager ||= AxeViolationManager.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.post_test_cleanup
|
34
|
+
manager.clear_failures_from_test
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.post_total_cleanup
|
38
|
+
manager.clear_failures_from_total
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.example_passed?
|
42
|
+
manager.test_passed?
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.suite_passed?
|
46
|
+
manager.total_passed?
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.summarize_results
|
50
|
+
manager.summarize_total_results_by_severity
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.detailed_results
|
54
|
+
manager.list_test_results
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.configuration
|
58
|
+
Stormbreaker.configuration
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#::Axe::Matchers::BeAxeClean.prepend AxeMatcherAuditor
|
4
|
+
module Stormbreaker
|
5
|
+
module AxeMatcherAuditor
|
6
|
+
def matches?(page)
|
7
|
+
audit(page)
|
8
|
+
# Since we want to compile our failures here rather than failing after the first violation,
|
9
|
+
# we'll return true each time instead of returning `audit.passed?` and retrieve the results
|
10
|
+
# to compile from audit
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ::RSpec::Core::Formatters::BaseTextFormatter.prepend AxeResultsReporter
|
4
|
+
module Stormbreaker
|
5
|
+
module AxeResultsReporter
|
6
|
+
# builds on built-in RSpec formatting via BaseTextFormatter#dump_summary
|
7
|
+
def dump_summary(summary)
|
8
|
+
warn Stormbreaker::AxeHelper.summarize_results unless Stormbreaker::AxeHelper.suite_passed?
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Stormbreaker
|
6
|
+
class AxeResultsSerializer
|
7
|
+
def self.serialize_results(total_violations)
|
8
|
+
dump = YAML.dump(total_violations)
|
9
|
+
prefix = Stormbreaker.configuration.serialize_prefix
|
10
|
+
filename = "#{prefix}_#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_#{Digest::SHA2.hexdigest(dump)}"
|
11
|
+
File.open(filename, 'w') do |f|
|
12
|
+
f.write(dump)
|
13
|
+
end
|
14
|
+
puts "Serialized results written to #{filename}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.combine_results(path = Dir.pwd)
|
18
|
+
prefix = Stormbreaker.configuration.serialize_prefix
|
19
|
+
results_files = Dir.glob("#{prefix}*", base: path)
|
20
|
+
raise "No results matching /#{prefix}*/ found in #{path}" if results_files.empty?
|
21
|
+
|
22
|
+
combined_manager = Stormbreaker::AxeViolationManager.new
|
23
|
+
results_files.each do |file|
|
24
|
+
total_violations = YAML.safe_load(File.read(File.join(path, file)), [Stormbreaker::AxeViolation, Set, Symbol])
|
25
|
+
total_violations.each do |violation|
|
26
|
+
combined_manager.add_failure_to_total(violation)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
combined_manager
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ::Axe::Matchers::BeAxeClean.prepend AxeMatcherAuditor
|
4
|
+
module Stormbreaker
|
5
|
+
module AxeRSpecAuditor
|
6
|
+
# Fire Axe assertion before normal Rspec assertion in case RSpec assertion fails and prevents Axe from running
|
7
|
+
def to(matcher = nil, message = nil, &block)
|
8
|
+
Stormbreaker::AxeHelper.assert_axe
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def not_to(matcher = nil, message = nil, &block)
|
13
|
+
Stormbreaker::AxeHelper.assert_axe
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stormbreaker
|
4
|
+
class AxeViolation
|
5
|
+
attr_accessor :called_by, :severity, :element, :violation, :complete_summary
|
6
|
+
|
7
|
+
def initialize(called_by: Set.new, severity: '', element: '', violation: '', complete_summary: '')
|
8
|
+
self.called_by = Set.new.merge(called_by)
|
9
|
+
self.severity = severity
|
10
|
+
self.violation = violation
|
11
|
+
self.element = element
|
12
|
+
self.complete_summary = complete_summary
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
called_by == other.called_by &&
|
17
|
+
severity == other.severity &&
|
18
|
+
violation == other.violation &&
|
19
|
+
element == other.element &&
|
20
|
+
complete_summary == other.complete_summary
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stormbreaker
|
4
|
+
class AxeViolationManager
|
5
|
+
attr_accessor :total_violations, :test_violations
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.total_violations = []
|
9
|
+
self.test_violations = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_failure(called_by: Set.new, violation: nil, node: nil)
|
13
|
+
axe_violation = get_violation(called_by: called_by, violation: violation, node: node)
|
14
|
+
add_failure_to_test(axe_violation)
|
15
|
+
add_failure_to_total(axe_violation)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_violation(called_by: Set.new, violation: nil, node: nil)
|
19
|
+
raise 'No violation found' unless violation && node
|
20
|
+
|
21
|
+
severity = violation.impact
|
22
|
+
element = node.html
|
23
|
+
rule = violation.description
|
24
|
+
complete_summary = node.failure_messages.flatten.join("\n")
|
25
|
+
|
26
|
+
AxeViolation.new(called_by: called_by, severity: severity, element: element, violation: rule,
|
27
|
+
complete_summary: complete_summary)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_failure_to_test(axe_violation)
|
31
|
+
@test_violations << axe_violation
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_failure_to_total(axe_violation)
|
35
|
+
match = @total_violations.select { |v| (v.violation == axe_violation.violation && v.element == axe_violation.element) }
|
36
|
+
raise "Multiple total_violations Found for #{v.description}" if match.count > 1
|
37
|
+
|
38
|
+
if match.any?
|
39
|
+
match.first.called_by = match.first.called_by.merge(axe_violation.called_by)
|
40
|
+
else
|
41
|
+
@total_violations << axe_violation
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear_failures_from_test
|
46
|
+
@test_violations = []
|
47
|
+
end
|
48
|
+
|
49
|
+
def clear_failures_from_total
|
50
|
+
@total_violations = []
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_passed?
|
54
|
+
@test_violations.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
def total_passed?
|
58
|
+
@total_violations.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def sort_test_violations
|
62
|
+
sorted = []
|
63
|
+
Stormbreaker.configuration.enabled_severity_categories.each do |severity|
|
64
|
+
sorted += @test_violations.select { |v| v.severity == severity }.sort_by(&:violation)
|
65
|
+
end
|
66
|
+
sorted
|
67
|
+
end
|
68
|
+
|
69
|
+
def list_test_results
|
70
|
+
summary = ''
|
71
|
+
prev_violation = ''
|
72
|
+
sort_test_violations.each do |violation|
|
73
|
+
summary += <<~INDENT
|
74
|
+
#{prev_violation != violation.violation ? "\n#{violation.violation} - Severity: #{violation.severity}\n" : ''}
|
75
|
+
#{violation.complete_summary.gsub("\n", "\n ")}
|
76
|
+
|
77
|
+
Violations Found In:
|
78
|
+
#{violation.called_by.to_a.join("\n")}
|
79
|
+
INDENT
|
80
|
+
prev_violation = violation.violation
|
81
|
+
end
|
82
|
+
summary
|
83
|
+
end
|
84
|
+
|
85
|
+
def summarize_total_results_by_severity
|
86
|
+
summary = ''
|
87
|
+
error_categories = Stormbreaker.configuration.enabled_severity_categories
|
88
|
+
unique_violations = 0
|
89
|
+
|
90
|
+
error_categories.each do |severity|
|
91
|
+
violation_map = {}
|
92
|
+
@total_violations.select { |v| v.severity == severity }.each do |error|
|
93
|
+
if violation_map[error.violation]
|
94
|
+
violation_map[error.violation] += error.called_by.count
|
95
|
+
else
|
96
|
+
unique_violations += 1
|
97
|
+
violation_map[error.violation] = error.called_by.count
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
next if violation_map.empty?
|
102
|
+
|
103
|
+
summary += "\n\n Level: #{severity}"
|
104
|
+
violation_map.each do |error, count|
|
105
|
+
summary += "\n - #{error} : #{count} instance(s)"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
"\nFound #{unique_violations} unique violation(s)" + summary
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stormbreaker
|
4
|
+
ALL_SEVERITY_CATEGORIES = %i[critical serious moderate minor].freeze
|
5
|
+
|
6
|
+
def self.configuration
|
7
|
+
@configuration ||= Configuration.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.reset!
|
11
|
+
@configuration = Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
yield(configuration)
|
16
|
+
end
|
17
|
+
|
18
|
+
class Configuration
|
19
|
+
attr_accessor :rules, :skip, :driver, :enabled_severity_categories, :results_path
|
20
|
+
attr_accessor :serialize_output, :serialized_input_path, :serialize_prefix
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@rules = %i[wcag2a wcag2aa section508]
|
24
|
+
@skip = %i[color-contrast duplicate-id]
|
25
|
+
@enabled_severity_categories = ALL_SEVERITY_CATEGORIES
|
26
|
+
@driver = nil
|
27
|
+
@results_path = 'results.html'
|
28
|
+
@serialize_output = false
|
29
|
+
@serialized_input_path = '.'
|
30
|
+
@serialize_prefix = 'stormbreaker_results'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configured_driver
|
35
|
+
# Driver may be a lambda
|
36
|
+
@configured_driver ||= if configuration.driver.respond_to?(:call)
|
37
|
+
configuration.driver.call
|
38
|
+
else
|
39
|
+
configuration.driver
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Stormbreaker
|
6
|
+
class ErbFormatter
|
7
|
+
include ERB::Util
|
8
|
+
attr_reader :violations
|
9
|
+
|
10
|
+
def initialize(violations)
|
11
|
+
@violations = sort_violations(violations)
|
12
|
+
@severity_categories = Stormbreaker.configuration.enabled_severity_categories
|
13
|
+
end
|
14
|
+
|
15
|
+
def sort_violations(violations)
|
16
|
+
sorted = {}
|
17
|
+
Stormbreaker.configuration.enabled_severity_categories.each do |severity|
|
18
|
+
sorted[severity] = violations.select { |v| v.severity == severity }
|
19
|
+
end
|
20
|
+
sorted
|
21
|
+
end
|
22
|
+
|
23
|
+
def write_to_erb
|
24
|
+
erb = ERB.new(template)
|
25
|
+
File.open(Stormbreaker.configuration.results_path, 'w') do |f|
|
26
|
+
f.write erb.result(binding)
|
27
|
+
end
|
28
|
+
puts "Results written to #{Stormbreaker.configuration.results_path}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def total_violations
|
32
|
+
@violations.reduce(0) { |key, (_category, violations)| key + violations.count }
|
33
|
+
end
|
34
|
+
|
35
|
+
def failing_spec_count(violation)
|
36
|
+
return if violation.called_by.to_a.empty?
|
37
|
+
|
38
|
+
violation.called_by.to_a.map { |spec| spec.to_s.match(/(.*):/) }.uniq.count
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_callstack(violation)
|
42
|
+
return if violation.called_by.to_a.empty?
|
43
|
+
|
44
|
+
violation.called_by.to_a.map { |spec| " #{spec}" }.join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def template
|
48
|
+
File.read(File.join(File.dirname(__FILE__), '../templates/results.erb'))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :stormbreaker do
|
4
|
+
desc 'Combines results that were created from isolated test runs'
|
5
|
+
task :combine_results, %i[path] do |_t, args|
|
6
|
+
require_relative '../../stormbreaker'
|
7
|
+
manager = Stormbreaker::AxeResultsSerializer.combine_results(args[:path])
|
8
|
+
erb_writer = Stormbreaker::ErbFormatter.new(manager.total_violations)
|
9
|
+
erb_writer.write_to_erb
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,104 @@
|
|
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">
|
6
|
+
<title>Stormbreaker</title>
|
7
|
+
|
8
|
+
<link rel="stylesheet" type="text/css" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"></link>
|
9
|
+
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.js"></script>
|
10
|
+
<script type="text/javascript" src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
|
11
|
+
|
12
|
+
<script type="text/javascript">
|
13
|
+
$( function() {
|
14
|
+
$( "#tabs" ).tabs();
|
15
|
+
} );
|
16
|
+
$( function () {
|
17
|
+
$(".accordion").click(function() {
|
18
|
+
console.log(this);
|
19
|
+
try {
|
20
|
+
$(".child").hide();
|
21
|
+
$(this).next("tr").toggle();
|
22
|
+
} catch (er) {
|
23
|
+
alert(er);
|
24
|
+
}
|
25
|
+
});
|
26
|
+
});
|
27
|
+
</script>
|
28
|
+
|
29
|
+
<style>
|
30
|
+
.child {
|
31
|
+
display: none;
|
32
|
+
}
|
33
|
+
.child-content {
|
34
|
+
font-size: 14px;
|
35
|
+
font-weight: lighter;
|
36
|
+
padding: 20px;
|
37
|
+
}
|
38
|
+
.accordion {
|
39
|
+
padding: 15px;
|
40
|
+
background: #f6f6f6;
|
41
|
+
text-align: center;
|
42
|
+
height: 30px;
|
43
|
+
font-weight: bold;
|
44
|
+
}
|
45
|
+
.header {
|
46
|
+
font-size: 18px;
|
47
|
+
}
|
48
|
+
</style>
|
49
|
+
|
50
|
+
</head>
|
51
|
+
<body>
|
52
|
+
<h1><%= total_violations %> Total Violations Found</h1>
|
53
|
+
<div id="tabs">
|
54
|
+
<ul>
|
55
|
+
<% @severity_categories.each_with_index do |f,index| %>
|
56
|
+
<li>
|
57
|
+
<a href="#tabs-<%= index+1 %>">
|
58
|
+
<%= f %>
|
59
|
+
</a>
|
60
|
+
</li>
|
61
|
+
<% end %>
|
62
|
+
</ul>
|
63
|
+
<% @severity_categories.each_with_index do |f,index| %>
|
64
|
+
<div id="tabs-<%= index+1 %>">
|
65
|
+
<h3><%= @violations[f].count %> Violation(s)</h3>
|
66
|
+
|
67
|
+
<table class="display" style="width:100%">
|
68
|
+
<thead>
|
69
|
+
<tr class="header">
|
70
|
+
<th>Selector</th>
|
71
|
+
<th>Category</th>
|
72
|
+
<th>Different Specs</th>
|
73
|
+
</tr>
|
74
|
+
</thead>
|
75
|
+
<tbody>
|
76
|
+
<% @violations[f].each do |v| %>
|
77
|
+
<tr class="accordion">
|
78
|
+
<td>
|
79
|
+
<%= html_escape(v.element) %>
|
80
|
+
</td>
|
81
|
+
<td>
|
82
|
+
<%= html_escape(v.violation) %>
|
83
|
+
</td>
|
84
|
+
<td>
|
85
|
+
<%= failing_spec_count(v) %>
|
86
|
+
</td>
|
87
|
+
</tr>
|
88
|
+
<tr class="child">
|
89
|
+
<td colspan="3" class="child-content">
|
90
|
+
<%= html_escape(v.complete_summary).gsub(/\n/, '<br />') %>
|
91
|
+
<br /><br /><br />
|
92
|
+
Failing Specs:
|
93
|
+
<br />
|
94
|
+
<%= parse_callstack(v).gsub(/\n/, '<br />') %>
|
95
|
+
</td>
|
96
|
+
</tr>
|
97
|
+
<% end %>
|
98
|
+
</tbody>
|
99
|
+
</table>
|
100
|
+
</div>
|
101
|
+
<% end %>
|
102
|
+
</div>
|
103
|
+
</body>
|
104
|
+
</html>
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Layout/ExtraSpacing, Layout/SpaceAroundOperators
|
4
|
+
require './lib/stormbreaker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'stormbreaker'
|
8
|
+
gem.version = Stormbreaker::VERSION
|
9
|
+
gem.license = 'MIT'
|
10
|
+
gem.authors = [
|
11
|
+
'Brian Watson'
|
12
|
+
]
|
13
|
+
gem.email = [
|
14
|
+
'bwatson@instructure.com'
|
15
|
+
]
|
16
|
+
gem.summary = 'Add axe assertions to expect statements by default in Ruby Selenium'
|
17
|
+
|
18
|
+
gem.files = `git ls-files -z`
|
19
|
+
.split("\x0")
|
20
|
+
.reject { |f| f.match(%r{^(test|spec|features|bin)/}) }
|
21
|
+
.reject { |f| f.match(/^(Jenkinsfile|Dockerfile|.dockerignore|.rspec|.rubocop)/) }
|
22
|
+
gem.bindir = 'bin'
|
23
|
+
gem.require_paths = ['lib']
|
24
|
+
|
25
|
+
gem.metadata['allowed_push_host'] = 'https://rubygems.org'
|
26
|
+
gem.required_ruby_version = '>= 2.6'
|
27
|
+
|
28
|
+
gem.add_dependency 'axe-core-api', '~> 4.1'
|
29
|
+
gem.add_dependency 'axe-core-rspec', '~> 4.1'
|
30
|
+
gem.add_dependency 'axe-core-selenium', '~> 4.1'
|
31
|
+
gem.add_dependency 'rspec', '~> 3.8'
|
32
|
+
gem.add_development_dependency 'bundler', '~> 1.17'
|
33
|
+
gem.add_development_dependency 'nokogiri', '~> 1.11.7'
|
34
|
+
gem.add_development_dependency 'pry', '~> 0.14.1'
|
35
|
+
gem.add_development_dependency 'rake', '~> 12.3'
|
36
|
+
gem.add_development_dependency 'simplecov', '~> 0.17'
|
37
|
+
end
|
38
|
+
# rubocop:enable Layout/ExtraSpacing, Layout/SpaceAroundOperators
|
metadata
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stormbreaker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Watson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-06-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: axe-core-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: axe-core-rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: axe-core-selenium
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.8'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.17'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.17'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: nokogiri
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.11.7
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.11.7
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.14.1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.14.1
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '12.3'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '12.3'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.17'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.17'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- bwatson@instructure.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- Gemfile
|
148
|
+
- LICENSE.txt
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- lib/stormbreaker.rb
|
152
|
+
- lib/stormbreaker/axe_helper.rb
|
153
|
+
- lib/stormbreaker/axe_matcher_auditor.rb
|
154
|
+
- lib/stormbreaker/axe_results_reporter.rb
|
155
|
+
- lib/stormbreaker/axe_results_serializer.rb
|
156
|
+
- lib/stormbreaker/axe_rspec_auditor.rb
|
157
|
+
- lib/stormbreaker/axe_violation.rb
|
158
|
+
- lib/stormbreaker/axe_violation_manager.rb
|
159
|
+
- lib/stormbreaker/configuration.rb
|
160
|
+
- lib/stormbreaker/erb_formatter.rb
|
161
|
+
- lib/stormbreaker/tasks/results.rake
|
162
|
+
- lib/stormbreaker/version.rb
|
163
|
+
- lib/templates/results.erb
|
164
|
+
- stormbreaker.gemspec
|
165
|
+
homepage:
|
166
|
+
licenses:
|
167
|
+
- MIT
|
168
|
+
metadata:
|
169
|
+
allowed_push_host: https://rubygems.org
|
170
|
+
post_install_message:
|
171
|
+
rdoc_options: []
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '2.6'
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
requirements: []
|
185
|
+
rubygems_version: 3.0.1
|
186
|
+
signing_key:
|
187
|
+
specification_version: 4
|
188
|
+
summary: Add axe assertions to expect statements by default in Ruby Selenium
|
189
|
+
test_files: []
|