axe-core-api 2.6.1.pre.0f0b25b

Sign up to get free protection for your applications and to get access to all the features.
@@ -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