axe-matchers 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/axe/api.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Axe
2
+ module API
3
+ LIBRARY_IDENTIFIER = "axe"
4
+ end
5
+ end
@@ -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,9 @@
1
+ require 'virtus'
2
+
3
+ module Axe
4
+ module API
5
+ class ValueObject
6
+ include ::Virtus.value_object mass_assignment: false, nullify_blank: true
7
+ end
8
+ end
9
+ 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