axe-matchers 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +362 -0
- data/README.md +72 -0
- data/lib/axe.rb +17 -0
- data/lib/axe/api.rb +5 -0
- data/lib/axe/api/a11y_check.rb +48 -0
- data/lib/axe/api/audit.rb +25 -0
- data/lib/axe/api/context.rb +60 -0
- data/lib/axe/api/options.rb +34 -0
- data/lib/axe/api/results.rb +23 -0
- data/lib/axe/api/results/check.rb +24 -0
- data/lib/axe/api/results/checked_node.rb +24 -0
- data/lib/axe/api/results/node.rb +21 -0
- data/lib/axe/api/results/rule.rb +27 -0
- data/lib/axe/api/rules.rb +43 -0
- data/lib/axe/api/value_object.rb +9 -0
- data/lib/axe/configuration.rb +42 -0
- data/lib/axe/cucumber/step.rb +89 -0
- data/lib/axe/cucumber/step_definitions.rb +3 -0
- data/lib/axe/javascript_library.rb +26 -0
- data/lib/axe/matchers.rb +1 -0
- data/lib/axe/matchers/be_accessible.rb +62 -0
- data/lib/axe/page.rb +26 -0
- data/lib/axe/rspec.rb +6 -0
- data/lib/axe/version.rb +3 -0
- data/lib/webdriver_script_adapter/exec_eval_script_adapter.rb +27 -0
- data/lib/webdriver_script_adapter/execute_async_script_adapter.rb +90 -0
- data/node_modules/axe-core/axe.min.js +25 -0
- metadata +270 -0
data/lib/axe/api.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'axe/api'
|
5
|
+
require 'axe/api/audit'
|
6
|
+
require 'axe/api/context'
|
7
|
+
require 'axe/api/options'
|
8
|
+
require 'axe/api/results'
|
9
|
+
require 'axe/javascript_library'
|
10
|
+
|
11
|
+
module Axe
|
12
|
+
module API
|
13
|
+
class A11yCheck
|
14
|
+
METHOD_NAME = "#{LIBRARY_IDENTIFIER}.a11yCheck"
|
15
|
+
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
def_delegators :@context, :include, :exclude
|
19
|
+
def_delegators :@options, :rules_by_tags, :run_rules, :skip_rules, :run_only_rules, :custom_options
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@context = Context.new
|
23
|
+
@options = Options.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(page)
|
27
|
+
inject_axe_lib page
|
28
|
+
audit page do |results|
|
29
|
+
Audit.new to_js, Results.new(results)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def inject_axe_lib(page)
|
36
|
+
JavaScriptLibrary.new.inject_into page
|
37
|
+
end
|
38
|
+
|
39
|
+
def audit(page)
|
40
|
+
yield page.execute_async_script "#{METHOD_NAME}.apply(#{LIBRARY_IDENTIFIER}, arguments)", @context.to_json, @options.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_js
|
44
|
+
"#{METHOD_NAME}(#{@context}, #{@options}, callback);"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Axe
|
2
|
+
module API
|
3
|
+
class Audit
|
4
|
+
|
5
|
+
attr_reader :invocation, :results
|
6
|
+
|
7
|
+
def initialize(invocation, results)
|
8
|
+
@invocation = invocation
|
9
|
+
@results = results
|
10
|
+
end
|
11
|
+
|
12
|
+
def passed?
|
13
|
+
results.violations.count == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure_message
|
17
|
+
"#{results.failure_message}\nInvocation: #{invocation}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def failure_message_when_negated
|
21
|
+
"Expected to find accessibility violations. None were detected.\nInvocation: #{invocation}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Axe
|
4
|
+
module API
|
5
|
+
class Context
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :inclusion, :exclusion
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@inclusion = []
|
12
|
+
@exclusion = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def include(selector)
|
16
|
+
@inclusion.concat ensure_nested_array(selector)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def exclude(selector)
|
21
|
+
@exclusion.concat ensure_nested_array(selector)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
{}.tap do |context_param|
|
27
|
+
# include key must not be included if empty
|
28
|
+
# (when undefined, defaults to `document`)
|
29
|
+
context_param[:include] = @inclusion unless @inclusion.empty?
|
30
|
+
|
31
|
+
# exclude array allowed to be empty
|
32
|
+
# and must exist in case `include` is omitted
|
33
|
+
# because context_param cannot be empty object ({})
|
34
|
+
context_param[:exclude] = @exclusion
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_json
|
39
|
+
if @inclusion.empty?
|
40
|
+
if @exclusion.empty?
|
41
|
+
"document"
|
42
|
+
else
|
43
|
+
%Q({"include":document,"exclude":#{@exclusion.to_json}})
|
44
|
+
end
|
45
|
+
else
|
46
|
+
to_hash.to_json
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
alias :to_s :to_json
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def ensure_nested_array(selector)
|
55
|
+
Array(selector).map { |s| Array(s) }
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'axe/api/rules'
|
3
|
+
|
4
|
+
module Axe
|
5
|
+
module API
|
6
|
+
class Options
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegator :@rules, :by_tags, :rules_by_tags
|
10
|
+
def_delegator :@rules, :run_only, :run_only_rules
|
11
|
+
def_delegator :@rules, :run, :run_rules
|
12
|
+
def_delegator :@rules, :skip, :skip_rules
|
13
|
+
def_delegator :@custom, :merge!, :custom_options
|
14
|
+
|
15
|
+
attr_reader :rules, :custom
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@rules = Rules.new
|
19
|
+
@custom = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
@rules.to_hash.merge(@custom)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json
|
27
|
+
to_hash.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
alias :to_s :to_json
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'axe/api/value_object'
|
2
|
+
|
3
|
+
module Axe
|
4
|
+
module API
|
5
|
+
class Results < ValueObject
|
6
|
+
require 'axe/api/results/rule'
|
7
|
+
|
8
|
+
values do
|
9
|
+
attribute :url, ::String
|
10
|
+
attribute :timestamp
|
11
|
+
attribute :passes, ::Array[Rule]
|
12
|
+
attribute :violations, ::Array[Rule]
|
13
|
+
end
|
14
|
+
|
15
|
+
def failure_message
|
16
|
+
<<-MSG.gsub(/^\s*/,'')
|
17
|
+
Found #{violations.count} accessibility #{violations.count == 1 ? 'violation' : 'violations'}
|
18
|
+
#{ violations.each_with_index.map(&:failure_message).join("\n\n") }
|
19
|
+
MSG
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'axe/api/value_object'
|
2
|
+
require 'axe/api/results/node'
|
3
|
+
|
4
|
+
module Axe
|
5
|
+
module API
|
6
|
+
class Results
|
7
|
+
class Check < ValueObject
|
8
|
+
values do
|
9
|
+
attribute :id, ::Symbol
|
10
|
+
attribute :impact, ::Symbol
|
11
|
+
attribute :message, ::String
|
12
|
+
attribute :data, ::String
|
13
|
+
attribute :relatedNodes, ::Array[Node]
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure_message
|
17
|
+
<<-MSG
|
18
|
+
#{message}
|
19
|
+
MSG
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'axe/api/results/node'
|
2
|
+
require 'axe/api/results/check'
|
3
|
+
|
4
|
+
module Axe
|
5
|
+
module API
|
6
|
+
class Results
|
7
|
+
class CheckedNode < Node
|
8
|
+
values do
|
9
|
+
attribute :impact, ::Symbol
|
10
|
+
attribute :any, ::Array[Check]
|
11
|
+
attribute :all, ::Array[Check]
|
12
|
+
attribute :none, ::Array[Check]
|
13
|
+
end
|
14
|
+
|
15
|
+
def failure_message
|
16
|
+
<<-MSG
|
17
|
+
#{super}
|
18
|
+
#{[].concat(any).concat(all).map(&:failure_message).join("\n")}
|
19
|
+
MSG
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'axe/api/value_object'
|
2
|
+
|
3
|
+
module Axe
|
4
|
+
module API
|
5
|
+
class Results
|
6
|
+
class Node < ValueObject
|
7
|
+
values do
|
8
|
+
attribute :html, ::String
|
9
|
+
attribute :target #String or Array[String]
|
10
|
+
end
|
11
|
+
|
12
|
+
def failure_message
|
13
|
+
<<-MSG
|
14
|
+
#{Array(target).join(', ')}
|
15
|
+
#{html}
|
16
|
+
MSG
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'axe/api/value_object'
|
2
|
+
require 'axe/api/results/checked_node'
|
3
|
+
|
4
|
+
module Axe
|
5
|
+
module API
|
6
|
+
class Results
|
7
|
+
class Rule < ValueObject
|
8
|
+
values do
|
9
|
+
attribute :id, ::Symbol
|
10
|
+
attribute :description, ::String
|
11
|
+
attribute :help, ::String
|
12
|
+
attribute :helpUrl, ::String
|
13
|
+
attribute :impact, ::Symbol
|
14
|
+
attribute :tags, ::Array[::Symbol]
|
15
|
+
attribute :nodes, ::Array[CheckedNode]
|
16
|
+
end
|
17
|
+
|
18
|
+
def failure_message(index)
|
19
|
+
<<-MSG
|
20
|
+
#{index+1}) #{help}: #{helpUrl}
|
21
|
+
#{nodes.map(&:failure_message).join("\n")}
|
22
|
+
MSG
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Axe
|
2
|
+
module API
|
3
|
+
class Rules
|
4
|
+
attr_reader :tags, :included, :excluded, :exclusive
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@tags = []
|
8
|
+
@included = []
|
9
|
+
@excluded = []
|
10
|
+
@exclusive = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def by_tags(tags)
|
14
|
+
@tags += tags
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_only(rules)
|
19
|
+
@exclusive += rules
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(rules)
|
24
|
+
@included += rules
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def skip(rules)
|
29
|
+
@excluded += rules
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_hash
|
34
|
+
{}.tap do |options|
|
35
|
+
#TODO warn that tags + exclusive-rules are incompatible
|
36
|
+
options.merge! runOnly: { type: :tag, values: @tags } unless @tags.empty?
|
37
|
+
options.merge! runOnly: { type: :rule, values: @exclusive } unless @exclusive.empty?
|
38
|
+
options.merge! rules: Hash[@included.product([enabled: true]) + @excluded.product([enabled: false])] unless @included.empty? && @excluded.empty?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'webdriver_script_adapter/execute_async_script_adapter'
|
3
|
+
|
4
|
+
module Axe
|
5
|
+
class Configuration
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_accessor :page
|
9
|
+
def_delegators ::WebDriverScriptAdapter,
|
10
|
+
:async_results_identifier, :async_results_identifier=,
|
11
|
+
:max_wait_time, :max_wait_time=,
|
12
|
+
:wait_interval, :wait_interval=
|
13
|
+
|
14
|
+
def page_from(world)
|
15
|
+
page_from_eval(world) ||
|
16
|
+
page ||
|
17
|
+
default_page_from(world) ||
|
18
|
+
from_ivar(:@page, world) ||
|
19
|
+
from_ivar(:@browser, world) ||
|
20
|
+
from_ivar(:@driver, world) ||
|
21
|
+
from_ivar(:@webdriver, world) ||
|
22
|
+
NullWebDriver.new
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def page_from_eval(world)
|
28
|
+
world.instance_eval "#{page}" if page.is_a?(String) || page.is_a?(Symbol)
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_page_from(world)
|
32
|
+
world.page if world.respond_to? :page
|
33
|
+
end
|
34
|
+
|
35
|
+
def from_ivar(ivar, world)
|
36
|
+
self.page = ivar
|
37
|
+
page_from_eval(world)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class NullWebDriver; end
|
42
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'axe'
|
4
|
+
require 'axe/matchers/be_accessible'
|
5
|
+
|
6
|
+
# The purpose of this class is to enable private helper methods for assertion
|
7
|
+
# and cucumber argument parsing without leaking the helper methods into the
|
8
|
+
# cucumber World object.
|
9
|
+
# Further, using these helper methods for assert/refute removes the dependency
|
10
|
+
# on rspec. So end users may choose to use any (or non) assertion/expectation
|
11
|
+
# library, as this class uses the Axe Accessibility Matcher directly, without
|
12
|
+
# using a matcher/expectation library DSL.
|
13
|
+
module Axe
|
14
|
+
module Cucumber
|
15
|
+
class Step
|
16
|
+
REGEX = /^
|
17
|
+
|
18
|
+
# require initial phrasing, with 'not' to negate the matcher
|
19
|
+
(?-x:the page should(?<negate> not)? be accessible)
|
20
|
+
|
21
|
+
# optionally specify which subtree to check, via CSS selector
|
22
|
+
(?-x:;? within "(?<inclusion>.*?)")?
|
23
|
+
|
24
|
+
# optionally specify subtrees to be excluded, via CSS selector
|
25
|
+
(?-x:;?(?: but)? excluding "(?<exclusion>.*?)")?
|
26
|
+
|
27
|
+
# optionally specify ruleset via list of comma-separated tags
|
28
|
+
(?-x:;? according to: (?<tags>.*?))?
|
29
|
+
|
30
|
+
# optionally specify rules (as comma-separated list of rule ids) to check
|
31
|
+
# in addition to default ruleset or explicit ruleset specified above via tags
|
32
|
+
# if the 'only' keyword is supplied, then *only* the listed rules are checked, not *additionally*
|
33
|
+
(?-x:;?(?: and)? checking(?<run_only> only)?: (?<run_rules>.*?))?
|
34
|
+
|
35
|
+
# optionally specify rules (as comma-separated list of rule ids) to skip
|
36
|
+
(?-x:;?(?: but)? skipping: (?<skip_rules>.*?))?
|
37
|
+
|
38
|
+
# optionally specify custom options (as a yaml-parsed hash or json string) to pass directly to axe-core
|
39
|
+
(?-x:;? with options: (?<options>.*?))?
|
40
|
+
|
41
|
+
$/x
|
42
|
+
|
43
|
+
def self.create_for(world)
|
44
|
+
new Axe.page_from world
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(page)
|
48
|
+
@page = page
|
49
|
+
end
|
50
|
+
|
51
|
+
def be_accessible(negate, inclusion, exclusion, tags, run_only, run_rules, skip_rules, options)
|
52
|
+
accessibility = Matchers::BeAccessible.new
|
53
|
+
|
54
|
+
accessibility.within(selector inclusion) if inclusion
|
55
|
+
accessibility.excluding(selector exclusion) if exclusion
|
56
|
+
accessibility.according_to(split tags) if tags
|
57
|
+
run_only ? accessibility.checking_only(split run_rules) : accessibility.checking(split run_rules) if run_rules
|
58
|
+
accessibility.skipping(split skip_rules) if skip_rules
|
59
|
+
accessibility.with_options(to_hash options) if options
|
60
|
+
|
61
|
+
if negate then refute accessibility else assert accessibility end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :page
|
67
|
+
|
68
|
+
def selector(selector)
|
69
|
+
split(selector)
|
70
|
+
end
|
71
|
+
|
72
|
+
def split(string)
|
73
|
+
String(string).split(/,\s*/)
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_hash(string)
|
77
|
+
YAML.load string
|
78
|
+
end
|
79
|
+
|
80
|
+
def assert(matcher)
|
81
|
+
raise matcher.failure_message unless matcher.matches? page
|
82
|
+
end
|
83
|
+
|
84
|
+
def refute(matcher)
|
85
|
+
raise matcher.failure_message_when_negated if matcher.matches? page
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|