stormbreaker 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ /coverage
2
+ /Gemfile.lock
3
+ /stormbreaker-*.gem
4
+ /pkg
5
+ results*.html
6
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
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 }
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stormbreaker
4
+ VERSION = '0.0.0'
5
+ 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: []