axe-core-api 2.6.1.pre.78a535c
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.
- checksums.yaml +7 -0
- data/LICENSE +362 -0
- data/README.md +5 -0
- data/lib/axe/api/a11y_check.rb +69 -0
- data/lib/axe/api/audit.rb +24 -0
- data/lib/axe/api/context.rb +35 -0
- data/lib/axe/api/options.rb +32 -0
- data/lib/axe/api/results.rb +48 -0
- data/lib/axe/api/results/check.rb +32 -0
- data/lib/axe/api/results/checked_node.rb +51 -0
- data/lib/axe/api/results/node.rb +35 -0
- data/lib/axe/api/results/rule.rb +58 -0
- data/lib/axe/api/rules.rb +37 -0
- data/lib/axe/api/run.rb +53 -0
- data/lib/axe/api/selector.rb +18 -0
- data/lib/axe/api/value_object.rb +9 -0
- data/lib/axe/configuration.rb +44 -0
- data/lib/axe/core.rb +45 -0
- data/lib/axe/dsl.rb +15 -0
- data/lib/axe/expectation.rb +33 -0
- data/lib/axe/finds_page.rb +55 -0
- data/lib/axe/matchers/be_accessible.rb +36 -0
- data/lib/chain_mail/chainable.rb +19 -0
- data/lib/hooks.rb +31 -0
- data/lib/loader.rb +25 -0
- data/lib/webdriver_script_adapter/exec_eval_script_adapter.rb +27 -0
- data/lib/webdriver_script_adapter/execute_async_script_adapter.rb +94 -0
- data/lib/webdriver_script_adapter/frame_adapter.rb +84 -0
- data/lib/webdriver_script_adapter/query_selector_adapter.rb +17 -0
- data/node_modules/axe-core/axe.min.js +12 -0
- metadata +200 -0
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
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "dumb_delegator"
|
2
|
+
|
3
|
+
module WebDriverScriptAdapter
|
4
|
+
class FrameAdapter < ::DumbDelegator
|
5
|
+
def self.wrap(driver)
|
6
|
+
if driver.respond_to?(:within_frame)
|
7
|
+
CapybaraAdapter.new driver
|
8
|
+
elsif !driver.respond_to?(:switch_to)
|
9
|
+
WatirAdapter.new driver
|
10
|
+
elsif driver.switch_to.respond_to?(:parent_frame)
|
11
|
+
SeleniumAdapter.new driver # add within_frame to selenium
|
12
|
+
else
|
13
|
+
ParentlessFrameAdapter.new driver # old selenium doesn't support parent_frame
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
class CapybaraAdapter < ::DumbDelegator
|
20
|
+
def find_frames
|
21
|
+
all(:css, "iframe")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class WatirAdapter < ::DumbDelegator
|
26
|
+
# delegate to Watir's Selenium #driver
|
27
|
+
def within_frame(frame, &block)
|
28
|
+
SeleniumAdapter.instance_method(:within_frame).bind(FrameAdapter.wrap driver).call(frame, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_frames
|
32
|
+
driver.find_elements(:css, "iframe")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class SeleniumAdapter < ::DumbDelegator
|
37
|
+
def within_frame(frame)
|
38
|
+
switch_to.frame(frame)
|
39
|
+
yield
|
40
|
+
ensure
|
41
|
+
begin
|
42
|
+
switch_to.parent_frame
|
43
|
+
rescue => e
|
44
|
+
if /switchToParentFrame|frame\/parent/.match(e.message)
|
45
|
+
::Kernel.warn "WARNING:
|
46
|
+
This browser only supports first-level iframes.
|
47
|
+
Second-level iframes and beyond will not be audited.
|
48
|
+
To skip auditing all iframes,
|
49
|
+
set Axe::Configuration#skip_iframes=true"
|
50
|
+
end
|
51
|
+
switch_to.default_content
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_frames
|
56
|
+
find_elements(:css, "iframe")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Selenium Webdriver < 2.43 doesnt support moving back to the parent
|
61
|
+
class ParentlessFrameAdapter < ::DumbDelegator
|
62
|
+
# storage of frame stack (for reverting to parent) taken from Capybara
|
63
|
+
# : https://github.com/jnicklas/capybara/blob/2.6.2/lib/capybara/selenium/driver.rb#L117-L147
|
64
|
+
#
|
65
|
+
# There doesnt appear to be any way in Selenium Webdriver < 2.43 to move back to a parent frame
|
66
|
+
# other than going back to the root and then reiterating down
|
67
|
+
def within_frame(frame)
|
68
|
+
@frame_stack[window_handle] ||= []
|
69
|
+
@frame_stack[window_handle] << frame
|
70
|
+
|
71
|
+
switch_to.frame(frame)
|
72
|
+
yield
|
73
|
+
ensure
|
74
|
+
@frame_stack[window_handle].pop
|
75
|
+
switch_to.default_content
|
76
|
+
@frame_stack[window_handle].each { |f| switch_to.frame(f) }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_frames
|
81
|
+
find_elements(:css, "iframe")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|