axe-core-api 2.6.1.pre.acca0cb
Sign up to get free protection for your applications and to get access to all the features.
- 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
|