axe-core-api 2.6.1.pre.78a535c

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