axe-core-gems icon indicating copy to clipboard operation
axe-core-gems copied to clipboard

BUG: undefined method `manage' for an instance of WebDriverScriptAdapter::QuerySelectorAdapter

Open amkisko opened this issue 1 year ago • 1 comments

Describe the bug

I tried to use axe-core-rspec in pair with capybara-playwright-driver.

After reading the code, I could not figure out from which part method manage should come and there are no visible traces if it ever was in WebDriverScriptAdapter::QuerySelectorAdapter.

Any suggestions on how to fix this error?

To Reproduce

Gemfile

gem "axe-core-rspec"
gem "capybara", require: false
gem "capybara-playwright-driver", require: false

rspec test case

require "capybara"
require "capybara-playwright-driver"
require "playwright"

Capybara.register_driver(:playwright) do |app|
  Capybara::Playwright::Driver.new(
    app,
    timeout: Capybara.default_max_wait_time,
    browser_type: ENV.fetch("BROWSER", "chromium").to_sym, # :chromium (default) or :firefox, :webkit
    headless: ENV["HEADFUL"].nil? || !ENV["CI"].nil?, # true for headless mode (default), false for headful mode.
    playwright_cli_executable_path: "npx playwright"
  )
end
Capybara.default_driver = Capybara.javascript_driver = :playwright

it "has no accessibility errors" do
  expect(page).to be_axe_clean
end

Expected behavior It runs the validations.

Actual behavior Returns

NoMethodError:
        undefined method `manage' for an instance of WebDriverScriptAdapter::QuerySelectorAdapter

amkisko avatar Oct 22 '24 19:10 amkisko

Can confirm that this specific issue (undefined method manage for driver) also happens for Cuprite.

JanuszSunscrapers avatar Jan 09 '25 18:01 JanuszSunscrapers

Also trying to use axe with playwright via RSpec and hitting this issue. We ended up doing a somewhat creative workaround, by removing the rubygem and using the npm package directly. I'll share in case it's helpful:

bundle remove axe-core-rspec axe-core-capybara
npm install --save-dev axe-core

Add this in spec/support/matchers/be_axe_clean.rb:

# Ruby class to wrap the JSON results payload
class AxeResults
  Violation = Data.define(:id, :impact, :tags, :description, :help, :helpUrl)

  AXE_JS = <<~JS.freeze
    #{Rails.root.join("node_modules/axe-core/axe.min.js").read}

    axe.run().then(results => console.log(JSON.stringify(results)));
  JS

  def initialize(page)
    unless page.driver.is_a?(Capybara::Playwright::Driver)
      raise ArgumentError,
            "make sure to use the playwright driver with this matcher"
    end

    @page = page
  end

  def violations
    @violations ||=
      axe_results
        .fetch("violations")
        .map do |json|
          # omitting nodes because it clutters up the output
          Violation.new(**json.except("nodes"))
        end
  end

  private

  attr_reader :page

  # inject the axe JS into the page, wait for the results to be logged, parse the results, and return them
  def axe_results
    @axe_results ||=
      begin
        axe_results_console_message =
          page.driver.with_playwright_page do |playwright_page|
            playwright_page.expect_console_message(
              predicate: method(:console_message_contains_axe_results?)
            ) { playwright_page.add_script_tag(content: AXE_JS) }
          end
        JSON.parse(axe_results_console_message.text)
      end
  end

  # predicate method which identifies the console log that contains the axe results payload
  def console_message_contains_axe_results?(msg)
    JSON.parse(msg.text).dig("testRunner", "name") == "axe"
  rescue StandardError
    false
  end
end

RSpec::Matchers.define :be_axe_clean do
  match do |page|
    @actual = AxeResults.new(page)

    @actual.violations == []
  end

  failure_message { |actual| <<~MSG }
      Expected no axe violations, found #{actual.violations.count}

      #{actual.violations.join("\n\n")}
    MSG
end

And then, as before, write assertions like this:

visit root_path
expect(page).to be_axe_clean

Lots of limitations with this:

  1. the output could be improved
  2. it doesn't support any of the chainable clauses that axe-core-rspec does

So of course I'd love to see the gem support capybara-playwright-driver out of the box and would happily switch back if and when it does.

maxjacobson avatar Jul 17 '25 17:07 maxjacobson

It seems the issue comes from here: https://github.com/dequelabs/axe-core-gems/blob/4a4faa58399b827ec6eaa1854df2e420b34a49ec/packages/axe-core-api/lib/axe/api/run.rb#L35

The core api seems to consider a Selenium driver is used all the time.

Chambeur avatar Jul 25 '25 03:07 Chambeur

I've made some tweaks to the AxeResults class that allows exclusion rules to be set. Hope this helps someone!

class AxeResults
  Violation = Data.define(:id, :impact, :tags, :description, :help, :help_url)
  ExclusionRule = Data.define(:id, :selector)

  AXE_COMMAND = <<~COMMAND.freeze
    axe.run({ runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] }, elementRef: true })
  COMMAND

  AXE_JS = <<~JS.freeze
    #{Rails.root.join('node_modules/axe-core/axe.min.js').read}
    #{AXE_COMMAND}.then(results => console.log(JSON.stringify({ testRunner: results.testRunner, violations: results.violations })));
  JS

  def initialize(page, exclusions: [])
    unless page.driver.is_a?(Capybara::Playwright::Driver)
      raise ArgumentError,
            "make sure to use the playwright driver with this matcher"
    end

    @page = page
    @exclusions = exclusions
  end

  def violations
    @violations ||=
      remove_exclusions(axe_results.fetch("violations")).map do |json|
        Violation.new(
          id: json["id"],
          impact: json["impact"],
          tags: json["tags"],
          description: json["description"],
          help: json["help"],
          help_url: json["helpUrl"],
        )
      end
  end

private

  attr_reader :page

  # Remove any exclusions from the results
  def remove_exclusions(json)
    json.reject do |item|
      @exclusions.any? do |exclusion|
        exclusion.id == item["id"] && item["nodes"].any? { |node| html_matches_selector?(node["html"], exclusion.selector) }
      end
    end
  end

  def html_matches_selector?(html, selector)
    Nokogiri::HTML.parse(html).css(selector).any?
  end

  # inject the axe JS into the page, wait for the results to be logged, parse the results, and return them
  def axe_results
    @axe_results ||=
      begin
        axe_results_console_message =
          page.driver.with_playwright_page do |playwright_page|
            playwright_page.expect_console_message(
              predicate: method(:console_message_contains_axe_results?),
            ) { playwright_page.add_script_tag(content: AXE_JS) }
          end
        JSON.parse(axe_results_console_message.text)
      end
  end

  # predicate method which identifies the console log that contains the axe results payload
  def console_message_contains_axe_results?(msg)
    JSON.parse(msg.text).dig("testRunner", "name") == "axe"
  rescue StandardError
    false
  end
end

pezholio avatar Oct 22 '25 07:10 pezholio