axe-matchers 1.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.
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