axe-core-api 2.6.1.pre.acca0cb

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,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
@@ -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
@@ -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
@@ -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