Skip to content

Commit 5e22602

Browse files
committed
Support executing with :cuprite and :playwright drivers
In an effort to support more than Selenium-based drivers, this change modifies the underlying `axe.js` integration. Prior to this change, this gem relied entirely on the [axe-core-api][] dependency to execute the audit, collect the results, and raise any violations as testing exceptions. Unfortunately, that gem does not provide official support for drivers other than Selenium ([axe-core-gems#243][]). To account for that shortcoming, this commit changes the gem to integrate with the `axe.js` JavaScript code more directly, including changes that invoke the interface directly through a combination of Capybara's [execute_script][], [evaluate_script][], and [evaluate_async_script][]. In support of that integration, this commit also extracts the `AxeAuditor` and `RaiseReporter` classes for the sake of abstracting the auditing and reporting steps (and for future integrations with other styles of audit reporting). [axe-core-api]: https://github.com/dequelabs/axe-core-gems/tree/develop/packages/axe-core-api [axe-core-gems#243]: dequelabs/axe-core-gems#243 [execute_script]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session:execute_script [evaluate_script]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session:evaluate_script [evaluate_async_script]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session:evaluate_async_script
1 parent 719e95b commit 5e22602

File tree

14 files changed

+147
-28
lines changed

14 files changed

+147
-28
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11+
driver:
12+
- "cuprite"
13+
- "playwright"
14+
- "selenium_chrome_headless"
1115
ruby-version:
1216
- "3.2"
1317
- "3.3"
@@ -19,9 +23,10 @@ jobs:
1923
- "7.2"
2024

2125
env:
26+
DRIVER: ${{ matrix.driver }}
2227
RAILS_VERSION: ${{ matrix.rails-version }}
2328

24-
name: ${{ format('Tests (Ruby {0}, Rails {1})', matrix.ruby-version, matrix.rails-version) }}
29+
name: ${{ format('Tests (Ruby {0}, Rails {1}, {2})', matrix.ruby-version, matrix.rails-version, matrix.driver) }}
2530
runs-on: "ubuntu-latest"
2631

2732
steps:
@@ -31,6 +36,14 @@ jobs:
3136
ruby-version: ${{ matrix.ruby-version }}
3237
bundler-cache: true
3338

39+
- if: ${{ matrix.driver == 'playwright' }}
40+
uses: "actions/setup-node@v5"
41+
with:
42+
cache: "yarn"
43+
44+
- if: ${{ matrix.driver == 'playwright' }}
45+
run: yarn install && yarn playwright install
46+
3447
- run: bin/rails standard
3548
if: matrix.ruby-version != 3.2
3649
- run: bin/rails test:all

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- Add support for `:cuprite` and `:playwright` system test drivers
1011
- Drop support for [End of Life Ruby versions 3.0 and 3.1](https://www.ruby-lang.org/en/downloads/branches/)
1112
- Drop support for [End of Life Rails versions 6.1, 7.0, and 7.1](https://rubyonrails.org/maintenance)
1213

Gemfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ gemspec
77
# Start debugger with binding.b [https://github.com/ruby/debug]
88
# gem "debug", ">= 1.0.0"
99

10-
rails_version = ENV.fetch("RAILS_VERSION", "7.0")
10+
rails_version = ENV.fetch("RAILS_VERSION", "7.2")
1111

1212
rails_constraint = if rails_version == "main"
1313
{github: "rails/rails"}
@@ -20,4 +20,6 @@ gem "rspec-rails"
2020

2121
gem "puma"
2222
gem "standard", "~> 1.12"
23+
gem "capybara-playwright-driver", require: "capybara/playwright"
24+
gem "cuprite", require: "capybara/cuprite"
2325
gem "selenium-webdriver"

lib/capybara_accessibility_audit/auditor.rb renamed to lib/capybara_accessibility_audit/adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module CapybaraAccessibilityAudit
2-
class Auditor
2+
class Adapter
33
delegate_missing_to :@test
44

55
def initialize(test)

lib/capybara_accessibility_audit/audit_system_test_extensions.rb

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require "axe/matchers/be_axe_clean"
2-
31
module CapybaraAccessibilityAudit
42
module AuditSystemTestExtensions
53
extend ActiveSupport::Concern
@@ -19,7 +17,7 @@ module AuditSystemTestExtensions
1917
MODAL_METHODS.each do |method|
2018
define_method method do |*arguments, **options, &block|
2119
result = super(*arguments, **options) { skip_accessibility_audits(&block) }
22-
result.tap { Auditor.new(self).audit!(method) }
20+
result.tap { Adapter.new(self).audit!(method) }
2321
end
2422
end
2523
end
@@ -35,7 +33,7 @@ def inherited(descendant)
3533
def accessibility_audit_after(*methods)
3634
(methods.flatten.to_set - accessibility_audit_after_methods).each do |method|
3735
define_method method do |*arguments, **options, &block|
38-
super(*arguments, **options, &block).tap { Auditor.new(self).audit!(method) }
36+
super(*arguments, **options, &block).tap { Adapter.new(self).audit!(method) }
3937
end
4038

4139
accessibility_audit_after_methods << method
@@ -89,7 +87,7 @@ def skip_accessibility_violations(value, &block)
8987
accessibility_audit_options.skipping = skipping
9088
end
9189

92-
def assert_no_accessibility_violations(**options)
90+
def assert_no_accessibility_violations(auditor: @accessibility_audit_auditor, **options)
9391
options.assert_valid_keys(
9492
:according_to,
9593
:checking,
@@ -100,10 +98,7 @@ def assert_no_accessibility_violations(**options)
10098
)
10199
options.compact_blank!
102100

103-
axe_matcher = Axe::Matchers::BeAxeClean.new
104-
axe_matcher = options.inject(axe_matcher) { |matcher, option| matcher.public_send(*option) }
105-
106-
assert axe_matcher.matches?(page), axe_matcher.failure_message
101+
auditor.audit(**options)
107102
end
108103
end
109104
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require "axe/api/context"
2+
require "axe/api/options"
3+
require "axe/api/results"
4+
require "axe/configuration"
5+
6+
module CapybaraAccessibilityAudit
7+
class AxeAuditor
8+
class_attribute :source, instance_accessor: false, default: Axe::Configuration.instance.jslib
9+
10+
def initialize(page, reporter)
11+
@page = page
12+
@reporter = reporter
13+
end
14+
15+
def audit(**options)
16+
install
17+
18+
results = run(options)
19+
20+
@reporter.report Axe::API::Results.new(results)
21+
end
22+
23+
private
24+
25+
def run(config)
26+
context, options = split(config)
27+
28+
@page.evaluate_async_script <<~JS, context.to_h, options.to_h
29+
const [ context, options, callback ] = arguments
30+
31+
axe.run(context, options).then(callback)
32+
JS
33+
end
34+
35+
def split(config)
36+
context = Axe::API::Context.new
37+
options = Axe::API::Options.new
38+
39+
config.each do |name, value|
40+
case name
41+
when :within, :excluding then context.public_send(name, value)
42+
else options.public_send(name, value)
43+
end
44+
end
45+
46+
[context, options]
47+
end
48+
49+
def install
50+
@page.execute_script(self.class.source) unless installed?
51+
end
52+
53+
def installed?
54+
@page.evaluate_script <<~JS
55+
"axe" in window && typeof axe.run === "function"
56+
JS
57+
end
58+
end
59+
end

lib/capybara_accessibility_audit/engine.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module CapybaraAccessibilityAudit
22
class Engine < ::Rails::Engine
33
config.capybara_accessibility_audit = ActiveSupport::OrderedOptions.new
4+
config.capybara_accessibility_audit.auditor = AxeAuditor
45
config.capybara_accessibility_audit.audit_after = %i[
56
visit
67
click_button
@@ -9,6 +10,7 @@ class Engine < ::Rails::Engine
910
click_on
1011
]
1112
config.capybara_accessibility_audit.audit_enabled = true
13+
config.capybara_accessibility_audit.reporter = RaiseReporter
1214

1315
initializer "capybara_accessibility_audit.minitest" do |app|
1416
ActiveSupport.on_load :action_dispatch_system_test_case do
@@ -17,6 +19,13 @@ class Engine < ::Rails::Engine
1719
self.accessibility_audit_enabled = app.config.capybara_accessibility_audit.audit_enabled
1820

1921
accessibility_audit_after app.config.capybara_accessibility_audit.audit_after
22+
23+
setup do
24+
auditor_class = app.config.capybara_accessibility_audit.auditor
25+
reporter_class = app.config.capybara_accessibility_audit.reporter
26+
27+
@accessibility_audit_auditor = auditor_class.new(page, reporter_class.new(self))
28+
end
2029
end
2130
end
2231

@@ -32,6 +41,11 @@ class Engine < ::Rails::Engine
3241
self.accessibility_audit_enabled = app.config.capybara_accessibility_audit.audit_enabled
3342

3443
accessibility_audit_after app.config.capybara_accessibility_audit.audit_after
44+
45+
auditor_class = app.config.capybara_accessibility_audit.auditor
46+
reporter_class = app.config.capybara_accessibility_audit.reporter
47+
48+
@accessibility_audit_auditor = auditor_class.new(page, reporter_class.new(self))
3549
end
3650

3751
config.before(type: :system, &configure)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module CapybaraAccessibilityAudit
2+
class RaiseReporter
3+
def initialize(test)
4+
@test = test
5+
end
6+
7+
def report(results)
8+
@test.assert results.violations.empty?, -> { results.failure_message }
9+
end
10+
end
11+
end

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"devDependencies": {
3+
"playwright": "1.57.0"
4+
}
5+
}

spec/spec_helper.rb

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,11 @@
77

88
Dummy::Application.initialize!
99

10-
module FeatureSpecBackports
11-
def driven_by(name, **)
12-
Capybara.current_driver = Capybara.javascript_driver = name
13-
end
14-
end
15-
1610
RSpec.configure do |config|
1711
config.use_active_record = false
1812

1913
config.filter_rails_from_backtrace!
20-
21-
config.include FeatureSpecBackports, type: :feature
2214
end
2315

16+
Capybara.javascript_driver = ENV.fetch("DRIVER", "selenium_chrome_headless").to_sym
2417
Capybara.server = :puma, {Silent: true}

0 commit comments

Comments
 (0)