Class: AxeCuprite::Injector

Inherits:
Object
  • Object
show all
Defined in:
lib/axe/cuprite/injector.rb

Overview

Handles getting axe-core onto the page and running it — entirely through Capybara’s driver-neutral JS API, with a Ferrum fast-path for Cuprite.

This is the make-or-break class. Two things are easy to get wrong:

1. The async-callback convention. Ferrum's `evaluate_async` (and Capybara's
   `evaluate_async_script`, which Cuprite delegates to it) wraps your
   expression in a Promise and appends the resolve callback as the LAST
   entry of `arguments`. So we resolve via `arguments[arguments.length - 1]`.

2. The timeout. Ferrum wraps the promise in a `setTimeout(reject, wait*1000)`.
   Through `evaluate_async_script` that `wait` is Capybara.default_max_wait_time
   (often 2s) — far too short for `axe.run` on a real page. So on Cuprite we
   call Ferrum's `page.evaluate_async(expr, explicit_wait, *args)` DIRECTLY
   with our own timeout, decoupled from default_max_wait_time entirely.

Constant Summary collapse

RUN_JS =

JS run inside Ferrum’s promise wrapper. ‘arguments` is the axe context, `arguments` the run options, and the LAST argument is the resolve callback Ferrum appended. We slim the result down to a JSON-safe payload —never returning the full results object (passes/inapplicable can be huge).

<<~JS
  var ctx = arguments[0];
  var opts = arguments[1] || {};
  var done = arguments[arguments.length - 1];
  if (typeof window.axe === 'undefined' || typeof window.axe.run !== 'function') {
    done({ error: 'axe-core is not present on the page' });
    return;
  }
  var promise = ctx ? window.axe.run(ctx, opts) : window.axe.run(opts);
  promise.then(function (results) {
    done({
      violations: results.violations,
      incomplete: results.incomplete,
      url: results.url,
      timestamp: results.timestamp,
      testEngine: results.testEngine
    });
  }).catch(function (err) {
    done({ error: (err && err.message) ? err.message : String(err) });
  });
JS
PRESENCE_JS =

JS expression that reports whether axe is loaded and runnable.

"typeof window.axe !== 'undefined' && typeof window.axe.run === 'function'"

Instance Method Summary collapse

Constructor Details

#initialize(page, configuration = AxeCuprite.configuration) ⇒ Injector

Returns a new instance of Injector.



49
50
51
52
# File 'lib/axe/cuprite/injector.rb', line 49

def initialize(page, configuration = AxeCuprite.configuration)
  @page = page
  @config = configuration
end

Instance Method Details

#ensure_injected!(force: false) ⇒ Object

Ensure axe is present. Idempotent: does nothing if already injected (so repeated assertions on one page don’t re-send ~500KB), unless force:. Returns true if it actually injected, false if it was already there.



62
63
64
65
66
67
# File 'lib/axe/cuprite/injector.rb', line 62

def ensure_injected!(force: false)
  return false if !force && injected?

  inject_source!
  true
end

#inject_source!Object

Inject the vendored axe-core source into the page. Primary path is Capybara’s driver-neutral execute_script (which, on Cuprite, runs via CDP Runtime.evaluate and is not subject to the page’s CSP). If that path fails to land axe — e.g. a strict Content-Security-Policy — we fall back to Ferrum’s add_script_tag.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/axe/cuprite/injector.rb', line 74

def inject_source!
  source = AxeCuprite.axe_source

  begin
    @page.execute_script(source)
  rescue StandardError => e
    raise InjectionError, "Failed to inject axe-core: #{e.message}" unless try_add_script_tag(source)
  end

  return true if injected?

  # execute_script silently no-op'd (CSP, sandbox, ...). Try the tag fallback.
  if try_add_script_tag(source) && injected?
    true
  else
    raise InjectionError,
          "axe-core did not load after injection. A strict Content-Security-Policy " \
          "may be blocking script injection on the page under test."
  end
end

#injected?Boolean

Is axe-core present and runnable on the current page?

Returns:

  • (Boolean)


55
56
57
# File 'lib/axe/cuprite/injector.rb', line 55

def injected?
  @page.evaluate_script(PRESENCE_JS) == true
end

#run(context:, options:, timeout: nil) ⇒ Object

Run axe and return a Results object. Injects on demand if needed.



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/axe/cuprite/injector.rb', line 96

def run(context:, options:, timeout: nil)
  timeout ||= @config.timeout
  ensure_present!

  raw = evaluate_axe(context, options, timeout)
  if raw.is_a?(Hash) && raw["error"]
    raise AxeRunError, "axe.run failed: #{raw["error"]}"
  end

  Results.new(raw)
end