BUG: undefined method `manage' for an instance of WebDriverScriptAdapter::QuerySelectorAdapter
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
Can confirm that this specific issue (undefined method manage for driver) also happens for Cuprite.
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:
- the output could be improved
- 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.
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.
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