axe-core-api 2.6.1.pre.0f0b25b

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.
@@ -0,0 +1,58 @@
1
+ require_relative "../value_object"
2
+ require_relative "./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_messages(index)
19
+ [
20
+ title_message(index + 1),
21
+ *[
22
+ helpUrl,
23
+ node_count_message,
24
+ "",
25
+ nodes.reject { |n| n.nil? }.map(&:failure_messages).map { |n| n.push("") }.flatten.map(&indent),
26
+ ].flatten.map(&indent),
27
+ ]
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ description: description,
33
+ help: help,
34
+ helpUrl: helpUrl,
35
+ id: id,
36
+ impact: impact,
37
+ nodes: nodes.map(&:to_h),
38
+ tags: tags,
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def indent
45
+ ->(line) { line.prepend(" " * 4) unless line.nil? }
46
+ end
47
+
48
+ def title_message(count)
49
+ "#{count}) #{id}: #{help} (#{impact})"
50
+ end
51
+
52
+ def node_count_message
53
+ "The following #{nodes.length} #{nodes.length == 1 ? "node" : "nodes"} violate this rule:"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ require_relative "./value_object"
2
+
3
+ module Axe
4
+ module API
5
+ class Results < ValueObject
6
+ require_relative "./results/rule"
7
+
8
+ values do
9
+ attribute :inapplicable, ::Array[Rule]
10
+ attribute :incomplete, ::Array[Rule]
11
+ attribute :passes, ::Array[Rule]
12
+ attribute :timestamp
13
+ attribute :url, ::String
14
+ attribute :violations, ::Array[Rule]
15
+ end
16
+
17
+ def failure_message
18
+ [
19
+ "",
20
+ violation_count_message,
21
+ "",
22
+ violations_failure_messages,
23
+ ].flatten.join("\n")
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ inapplicable: inapplicable.map(&:to_h),
29
+ incomplete: incomplete.map(&:to_h),
30
+ passes: passes.map(&:to_h),
31
+ timestamp: timestamp,
32
+ url: url,
33
+ violations: violations.map(&:to_h),
34
+ }
35
+ end
36
+
37
+ private
38
+
39
+ def violation_count_message
40
+ "Found #{violations.count} accessibility #{violations.count == 1 ? "violation" : "violations"}:"
41
+ end
42
+
43
+ def violations_failure_messages
44
+ violations.each_with_index.map(&:failure_messages)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ module Axe
2
+ module API
3
+ class Rules
4
+ def initialize
5
+ @tags = []
6
+ @included = []
7
+ @excluded = []
8
+ @exclusive = []
9
+ end
10
+
11
+ def according_to(*tags)
12
+ @tags.concat tags.flatten
13
+ end
14
+
15
+ def checking(*rules)
16
+ @included.concat rules.flatten
17
+ end
18
+
19
+ def checking_only(*rules)
20
+ @exclusive.concat rules.flatten
21
+ end
22
+
23
+ def skipping(*rules)
24
+ @excluded.concat rules.flatten
25
+ end
26
+
27
+ def to_hash
28
+ {}.tap do |options|
29
+ # TODO warn that tags + exclusive-rules are incompatible
30
+ options.merge! runOnly: { type: :tag, values: @tags } unless @tags.empty?
31
+ options.merge! runOnly: { type: :rule, values: @exclusive } unless @exclusive.empty?
32
+ options.merge! rules: Hash[@included.product([enabled: true]) + @excluded.product([enabled: false])] unless @included.empty? && @excluded.empty?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,53 @@
1
+ require "forwardable"
2
+ require "json"
3
+
4
+ require_relative "../../chain_mail/chainable"
5
+ require_relative "./audit"
6
+ require_relative "./context"
7
+ require_relative "./options"
8
+ require_relative "./results"
9
+ require_relative "../core"
10
+
11
+ module Axe
12
+ module API
13
+ class Run
14
+ JS_NAME = "run"
15
+ METHOD_NAME = "#{Core::JS_NAME}.#{JS_NAME}"
16
+
17
+ extend Forwardable
18
+ def_delegators :@context, :within, :excluding
19
+ def_delegators :@options, :according_to, :checking, :checking_only, :skipping, :with_options
20
+
21
+ extend ChainMail::Chainable
22
+ chainable :within, :excluding, :according_to, :checking, :checking_only, :skipping, :with_options
23
+
24
+ def initialize
25
+ @context = Context.new
26
+ @options = Options.new
27
+ end
28
+
29
+ def call(page)
30
+ audit page do |results|
31
+ Audit.new to_js, Results.new(results)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def audit(page)
38
+ yield page.execute_async_script "#{METHOD_NAME}.apply(#{Core::JS_NAME}, arguments)", *js_args
39
+ end
40
+
41
+ def js_args
42
+ [@context, @options]
43
+ .reject(&:empty?)
44
+ .map(&:to_json)
45
+ end
46
+
47
+ def to_js
48
+ str_args = (js_args + ["callback"]).join(", ")
49
+ "#{METHOD_NAME}(#{str_args});"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ module Axe
2
+ module API
3
+ class Selector
4
+ def initialize(s)
5
+ @selector = case s
6
+ when Array then s
7
+ when String, Symbol then [String(s)]
8
+ when Hash then Selector.new(s[:selector]).to_a.unshift s[:iframe]
9
+ else Selector.new(s.selector).to_a.unshift s.iframe
10
+ end
11
+ end
12
+
13
+ def to_a
14
+ @selector
15
+ end
16
+ end
17
+ end
18
+ 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,44 @@
1
+ require "singleton"
2
+ require "forwardable"
3
+ require "json"
4
+
5
+ require_relative "../hooks"
6
+ require_relative "../webdriver_script_adapter/execute_async_script_adapter"
7
+
8
+ module Axe
9
+ class Configuration
10
+ include Singleton
11
+ include Common::Hooks
12
+ extend Forwardable
13
+
14
+ attr_writer :jslib
15
+ attr_accessor :page,
16
+ :jslib_path,
17
+ :skip_iframes
18
+ def_delegators ::WebDriverScriptAdapter,
19
+ :async_results_identifier,
20
+ :async_results_identifier=,
21
+ :max_wait_time,
22
+ :max_wait_time=,
23
+ :wait_interval,
24
+ :wait_interval=
25
+
26
+ # init
27
+ def initialize
28
+ @page = :page
29
+ @skip_iframes = :skip_iframes
30
+ @jslib_path = get_root + "/node_modules/axe-core/axe.min.js"
31
+ end
32
+
33
+ # jslib
34
+ def jslib
35
+ @jslib ||= Pathname.new(@jslib_path).read
36
+ end
37
+
38
+ private
39
+
40
+ def get_root
41
+ Gem::Specification.find_by_name('axe-core-api').gem_dir
42
+ end
43
+ end
44
+ end
data/lib/axe/core.rb ADDED
@@ -0,0 +1,45 @@
1
+ require_relative "../webdriver_script_adapter/execute_async_script_adapter"
2
+ require_relative "../webdriver_script_adapter/frame_adapter"
3
+ require_relative "../webdriver_script_adapter/query_selector_adapter"
4
+ require_relative "../loader"
5
+ require_relative "./configuration"
6
+
7
+ module Axe
8
+ class Core
9
+ JS_NAME = "axe"
10
+
11
+ def initialize(page)
12
+ @page = wrap_driver page
13
+ load_axe_core Axe::Configuration.instance.jslib
14
+ end
15
+
16
+ def call(callable)
17
+ callable.call(@page)
18
+ end
19
+
20
+ private
21
+
22
+ def load_axe_core(source)
23
+ Common::Loader.new(@page, self).call(source) unless already_loaded?
24
+ end
25
+
26
+ def already_loaded?
27
+ @page.evaluate_script <<-JS
28
+ window.#{JS_NAME} &&
29
+ typeof #{JS_NAME}.run === 'function'
30
+ JS
31
+ end
32
+
33
+ def wrap_driver(driver)
34
+ ::WebDriverScriptAdapter::QuerySelectorAdapter.wrap(
35
+ ::WebDriverScriptAdapter::FrameAdapter.wrap(
36
+ ::WebDriverScriptAdapter::ExecuteAsyncScriptAdapter.wrap(
37
+ ::WebDriverScriptAdapter::ExecEvalScriptAdapter.wrap(
38
+ driver
39
+ )
40
+ )
41
+ )
42
+ )
43
+ end
44
+ end
45
+ end
data/lib/axe/dsl.rb ADDED
@@ -0,0 +1,15 @@
1
+ require_relative "./matchers/be_accessible"
2
+ require_relative "./expectation"
3
+
4
+ module Axe
5
+ module DSL
6
+ module_function
7
+
8
+ # get the be_accessible matcher method
9
+ extend Matchers
10
+
11
+ def expect(page)
12
+ AccessibilityExpectation.new page
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ module Axe
2
+ class AccessibleExpectation
3
+ def assert(page, matcher)
4
+ raise matcher.failure_message unless matcher.matches? page
5
+ end
6
+ end
7
+
8
+ class InaccessibleExpectation
9
+ def assert(page, matcher)
10
+ raise matcher.failure_message_when_negated if matcher.matches? page
11
+ end
12
+ end
13
+
14
+ class AccessibilityExpectation
15
+ def self.create(negate)
16
+ negate ? InaccessibleExpectation.new : AccessibleExpectation.new
17
+ end
18
+
19
+ def initialize(page)
20
+ @page = page
21
+ end
22
+
23
+ def to(matcher)
24
+ AccessibleExpectation.new.assert @page, matcher
25
+ end
26
+
27
+ def to_not(matcher)
28
+ InaccessibleExpectation.new.assert @page, matcher
29
+ end
30
+
31
+ alias :not_to :to_not
32
+ end
33
+ end
@@ -0,0 +1,55 @@
1
+ require_relative "./configuration"
2
+
3
+ module Axe
4
+ class FindsPage
5
+ WEBDRIVER_NAMES = [:page, :browser, :driver, :webdriver]
6
+
7
+ class << self
8
+ alias :in :new
9
+ end
10
+
11
+ def initialize(world)
12
+ @world = world
13
+ end
14
+
15
+ def page
16
+ from_configuration || implicit or raise "A page/browser/webdriver must be configured"
17
+ end
18
+
19
+ private
20
+
21
+ def configuration
22
+ Axe::Configuration.instance
23
+ end
24
+
25
+ def from_configuration
26
+ if configuration.page.is_a?(String) || configuration.page.is_a?(Symbol)
27
+ from_world(configuration.page)
28
+ else
29
+ configuration.page
30
+ end
31
+ end
32
+
33
+ def implicit
34
+ WEBDRIVER_NAMES.map(&method(:from_world)).drop_while(&:nil?).first
35
+ end
36
+
37
+ def from_world(name)
38
+ via_method(name) || via_ivar(name)
39
+ end
40
+
41
+ def via_method(name)
42
+ @world.__send__(name) if @world.respond_to?(name)
43
+ end
44
+
45
+ def via_ivar(name)
46
+ name = ensure_ivar_format(name)
47
+ @world.instance_variable_get(name) if @world.instance_variables.include?(name)
48
+ end
49
+
50
+ def ensure_ivar_format(name)
51
+ # ensure leading '@'
52
+ name.to_s.sub(/^([^@])/, '@\1').to_sym
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+ require "forwardable"
2
+
3
+ require_relative "../../chain_mail/chainable"
4
+ require_relative "../core"
5
+ require_relative "../api/run"
6
+
7
+ module Axe
8
+ module Matchers
9
+ class BeAccessible
10
+ extend Forwardable
11
+ def_delegators :@audit, :failure_message, :failure_message_when_negated
12
+ def_delegators :@run, :within, :excluding, :according_to, :checking, :checking_only, :skipping, :with_options
13
+
14
+ extend ChainMail::Chainable
15
+ chainable :within, :excluding, :according_to, :checking, :checking_only, :skipping, :with_options
16
+
17
+ def initialize
18
+ @run = API::Run.new
19
+ end
20
+
21
+ def audit(page)
22
+ @audit ||= Core.new(page).call @run
23
+ end
24
+
25
+ def matches?(page)
26
+ audit(page).passed?
27
+ end
28
+ end
29
+
30
+ module_function
31
+
32
+ def be_accessible
33
+ BeAccessible.new
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ # TODO
2
+ # - able to be extended
3
+ # - able to be used without extending (module_function)
4
+ # - variant that returns nil instead of self
5
+ module ChainMail
6
+ module Chainable
7
+ module_function
8
+
9
+ def chainable(*methods)
10
+ methods.each do |method|
11
+ original = instance_method(method)
12
+ define_method method do |*args|
13
+ original.bind(self).call(*args)
14
+ self
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/hooks.rb ADDED
@@ -0,0 +1,31 @@
1
+ module Common
2
+ module Hooks
3
+ HOOKS = [:after_load]
4
+
5
+ HOOKS.each do |hook_name|
6
+ # define instance-level registration method per hook
7
+ define_method hook_name do |callable = nil, &block|
8
+ callable ||= block
9
+ Hooks.callbacks.fetch(hook_name) << callable if callable
10
+ end
11
+
12
+ # define singleton-level run_* method per hook
13
+ define_singleton_method "run_#{hook_name}" do |*args|
14
+ callbacks.fetch(hook_name).each do |callback|
15
+ callback.call(*args)
16
+ end
17
+ end
18
+ end
19
+
20
+ # beware, the callbacks hash is a single shared instance tied to this module
21
+ def self.callbacks
22
+ @callbacks ||= initialize_callbacks_array_per_hook
23
+ end
24
+
25
+ private
26
+
27
+ def self.initialize_callbacks_array_per_hook
28
+ Hash[HOOKS.map { |name| [name, []] }]
29
+ end
30
+ end
31
+ end
data/lib/loader.rb ADDED
@@ -0,0 +1,25 @@
1
+ require_relative "./axe/configuration"
2
+ require_relative "./hooks"
3
+
4
+ module Common
5
+ class Loader
6
+ def initialize(page, lib)
7
+ @page = page
8
+ @lib = lib
9
+ end
10
+
11
+ def call(source)
12
+ @page.execute_script source
13
+ Common::Hooks.run_after_load @lib
14
+ load_into_iframes(source) unless Axe::Configuration.instance.skip_iframes
15
+ end
16
+
17
+ private
18
+
19
+ def load_into_iframes(source)
20
+ @page.find_frames.each do |iframe|
21
+ @page.within_frame(iframe) { call source }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ require 'dumb_delegator'
2
+
3
+ module WebDriverScriptAdapter
4
+ # Capybara distinguishes eval from exec
5
+ # (eval is a query, exec is a command)
6
+ # this decorator makes webdriver act like capybara
7
+ class ExecEvalScriptAdapter < ::DumbDelegator
8
+ def self.wrap(driver)
9
+ raise WebDriverError, "WebDriver must respond to #execute_script" unless driver.respond_to? :execute_script
10
+
11
+ driver.respond_to?(:evaluate_script) ? driver : new(driver)
12
+ end
13
+
14
+ # executes script without returning result
15
+ def execute_script(script)
16
+ super
17
+ nil
18
+ end
19
+
20
+ # returns result of executing script
21
+ def evaluate_script(script)
22
+ __getobj__.execute_script "return #{script}"
23
+ end
24
+ end
25
+
26
+ class WebDriverError < TypeError; end
27
+ end
@@ -0,0 +1,94 @@
1
+ require "dumb_delegator"
2
+ require "securerandom"
3
+ require "timeout"
4
+ require_relative "./exec_eval_script_adapter"
5
+
6
+ module WebDriverScriptAdapter
7
+ class << self
8
+ attr_accessor :async_results_identifier,
9
+ :max_wait_time,
10
+ :wait_interval
11
+
12
+ def configure
13
+ yield self
14
+ end
15
+ end
16
+
17
+ module Defaults
18
+ module_function
19
+
20
+ def async_results_identifier
21
+ -> { ::SecureRandom.uuid }
22
+ end
23
+
24
+ # set max_wait_time based on type of webdriver
25
+ def max_wait_time
26
+ if defined? ::Capybara
27
+ if ::Capybara.respond_to? :default_max_wait_time
28
+ ::Capybara.default_max_wait_time
29
+ else
30
+ ::Capybara.default_wait_time
31
+ end
32
+ elsif defined? ::Selenium::WebDriver::Wait::DEFAULT_TIMEOUT
33
+ ::Selenium::WebDriver::Wait::DEFAULT_TIMEOUT
34
+ else
35
+ 3
36
+ end
37
+ end
38
+
39
+ # set wait interval based on webdriver
40
+ def wait_interval
41
+ if defined? ::Selenium::WebDriver::Wait::DEFAULT_INTERVAL
42
+ ::Selenium::WebDriver::Wait::DEFAULT_INTERVAL
43
+ else
44
+ 0.1
45
+ end
46
+ end
47
+ end
48
+
49
+ module ScriptWriter
50
+ module_function
51
+
52
+ def async_results_identifier
53
+ id = WebDriverScriptAdapter.async_results_identifier
54
+ "window['#{id.respond_to?(:call) ? id.call : id}']"
55
+ end
56
+
57
+ def callback(resultsIdentifier)
58
+ "function(err, returnValue){ #{resultsIdentifier} = (err || returnValue); }"
59
+ end
60
+
61
+ def async_wrapper(script, *args)
62
+ "(function(){ #{script} })(#{args.join(", ")});"
63
+ end
64
+ end
65
+
66
+ module Patiently
67
+ module_function
68
+
69
+ def wait_until
70
+ ::Timeout.timeout(WebDriverScriptAdapter.max_wait_time) do
71
+ sleep(WebDriverScriptAdapter.wait_interval) while (value = yield).nil?
72
+ value
73
+ end
74
+ end
75
+ end
76
+
77
+ class ExecuteAsyncScriptAdapter < ::DumbDelegator
78
+ def self.wrap(driver)
79
+ new ExecEvalScriptAdapter.wrap driver
80
+ end
81
+
82
+ def execute_async_script(script, *args)
83
+ results = ScriptWriter.async_results_identifier
84
+ execute_script ScriptWriter.async_wrapper(script, *args, ScriptWriter.callback(results))
85
+ Patiently.wait_until { evaluate_script results }
86
+ end
87
+ end
88
+
89
+ configure do |c|
90
+ c.async_results_identifier = Defaults.async_results_identifier
91
+ c.max_wait_time = Defaults.max_wait_time
92
+ c.wait_interval = Defaults.wait_interval
93
+ end
94
+ end