axe-cuprite 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -0
- data/README.md +72 -9
- data/lib/axe/cuprite/configuration.rb +8 -0
- data/lib/axe/cuprite/injector.rb +51 -14
- data/lib/axe/cuprite/results.rb +33 -4
- data/lib/axe/cuprite/rspec/matchers.rb +1 -1
- data/lib/axe/cuprite/runner.rb +1 -5
- data/lib/axe/cuprite/version.rb +1 -1
- data/lib/axe/cuprite.rb +4 -2
- data/lib/axe-cuprite.rb +0 -2
- metadata +5 -156
- data/lib/axe/cuprite/tasks/axe.rake +0 -73
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e56b57fadf23c8ab623bd1a3005d94ce158832160a9ec3bfd1591b070312d953
|
|
4
|
+
data.tar.gz: 2cdad2a18469b669266938df7476040d702b53419bb8a80076e918c282bf218e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2262ff93c4a88cda10c54d650b74f9353d8d1ae98f0f0a4df82b1483cc755e6347e23be923dedee4fffa726de3229ae4f4e9a00d18f704750f21513038d2515e
|
|
7
|
+
data.tar.gz: 8f001777544278d53266271b66c9d93bb32e0ed2d9e8969fa2413d69342649028be46c78c40706ec526e863c2fa13106a451856b7dfa7db8d23e0852b3367a9f
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,107 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.0] - 2026-06-16
|
|
11
|
+
|
|
12
|
+
First stable release. No functional changes since `0.2.0` — this tags the API as
|
|
13
|
+
stable under [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The
|
|
14
|
+
public API is the documented surface of `AxeCuprite`, `AxeCuprite::Configuration`,
|
|
15
|
+
`AxeCuprite::Runner`, the result wrappers (`Results`/`Violation`/`Node`/
|
|
16
|
+
`ContrastData`), the error classes, and the RSpec matcher DSL; classes such as
|
|
17
|
+
`AxeCuprite::Injector` and `AxeCuprite::DeepFreeze` are internal and not covered
|
|
18
|
+
by the stability guarantee.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- The packaged gem no longer ships the dev-only vendoring rake tasks
|
|
22
|
+
(`lib/axe/cuprite/tasks/*.rake` — `axe:update` / `axe:verify` / `axe:version`).
|
|
23
|
+
These operate on the gem's read-only, version-pinned `vendor/` directory and
|
|
24
|
+
`axe:verify` couldn't even run from an installed gem (its recorded checksum
|
|
25
|
+
isn't shipped), so they were broken for consumers. They remain available in a
|
|
26
|
+
repo checkout for development
|
|
27
|
+
([#32](https://github.com/Guided-Rails/axe-cuprite/issues/32)).
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Repositioned as **Cuprite-only**: the README, gemspec `description`, and
|
|
31
|
+
`AGENTS.md` no longer advertise support for "any real-browser Capybara driver."
|
|
32
|
+
Cuprite is the only supported and tested driver; the driver-neutral JS approach
|
|
33
|
+
is described as the implementation detail that makes the gem work on Cuprite
|
|
34
|
+
(and avoid Selenium internals), not a promise of broad driver support. The
|
|
35
|
+
best-effort non-Ferrum fallback in `Injector#evaluate_axe` is kept but is now
|
|
36
|
+
documented as unsupported/untested
|
|
37
|
+
([#31](https://github.com/Guided-Rails/axe-cuprite/issues/31)).
|
|
38
|
+
- Documented that `AxeCuprite::Results` (and its nested wrappers) takes ownership
|
|
39
|
+
of the hash passed in and deep-freezes it **in place** — so a caller
|
|
40
|
+
constructing `Results.new(hash)` shouldn't pass or reuse a hash they intend to
|
|
41
|
+
mutate afterward. No behavior change; clarifies the existing read-only contract
|
|
42
|
+
in the `Results` doc comment and the README runner section
|
|
43
|
+
([#33](https://github.com/Guided-Rails/axe-cuprite/issues/33)).
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- A unit test pinning the best-effort non-Ferrum fallback in
|
|
47
|
+
`Injector#evaluate_axe`: with no Ferrum page it routes through
|
|
48
|
+
`evaluate_async_script` under the explicit timeout rather than
|
|
49
|
+
`Capybara.default_max_wait_time`, so the path can't silently break
|
|
50
|
+
([#31](https://github.com/Guided-Rails/axe-cuprite/issues/31)).
|
|
51
|
+
|
|
52
|
+
## [0.2.0] - 2026-06-10
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- `config.include_html` (default `true`) — set to `false` to suppress the
|
|
56
|
+
truncated outer-HTML snippets that failure messages and `report_only` logs
|
|
57
|
+
print for each offending element. Useful for suites that render sensitive data
|
|
58
|
+
(staging-backed tests, seeded PII), keeping page content out of CI logs while
|
|
59
|
+
still reporting rule id, selector, and check message. Documented the snippet
|
|
60
|
+
behavior and `logger` guidance in the README
|
|
61
|
+
([#14](https://github.com/Guided-Rails/axe-cuprite/issues/14)).
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- The result wrappers (`Results`/`Violation`/`Node`/`ContrastData`) now honor
|
|
65
|
+
their documented read-only contract: `@raw` is deep-frozen at construction, so
|
|
66
|
+
`raw` and `to_h` can safely expose the live underlying hash without a caller
|
|
67
|
+
being able to mutate the wrapper's internal state (which, via memoization,
|
|
68
|
+
could previously desync `violations`/`incomplete` from `raw`)
|
|
69
|
+
([#17](https://github.com/Guided-Rails/axe-cuprite/issues/17)).
|
|
70
|
+
- `Injector#inject_source!` no longer hides the real cause of an injection
|
|
71
|
+
failure behind a blanket Content-Security-Policy message. When both the
|
|
72
|
+
primary `execute_script` path and the `add_script_tag` fallback fail, the
|
|
73
|
+
exceptions they raised are now captured and appended to the `InjectionError`
|
|
74
|
+
message (`Underlying errors: execute_script: ...; add_script_tag: ...`), so a
|
|
75
|
+
dead browser, dead CDP session, or misconfigured driver is no longer
|
|
76
|
+
misattributed to CSP ([#15](https://github.com/Guided-Rails/axe-cuprite/issues/15)).
|
|
77
|
+
- `Injector#timeout_error?` no longer reclassifies arbitrary failures as
|
|
78
|
+
`AxeCuprite::TimeoutError` just because the error message mentions a timeout.
|
|
79
|
+
A genuine page-side JavaScript error (e.g. a `Ferrum::JavaScriptError` from axe
|
|
80
|
+
or the app whose text happens to contain "timeout") now propagates untouched
|
|
81
|
+
instead of being rewritten with misleading "increase the timeout / scope the
|
|
82
|
+
run" guidance. Classification is driven by error class (Ferrum's
|
|
83
|
+
timeout/script-timeout classes), with a narrow class-gated message check only
|
|
84
|
+
for Ferrum's async-evaluation "timed out promise" case; this also removes a
|
|
85
|
+
dead code branch that could never affect the result
|
|
86
|
+
([#16](https://github.com/Guided-Rails/axe-cuprite/issues/16)).
|
|
87
|
+
|
|
88
|
+
### Security
|
|
89
|
+
- `rake axe:update` now vendors axe-core from the official npm registry tarball
|
|
90
|
+
(`registry.npmjs.org`) instead of the unpkg CDN, verifies the tarball against
|
|
91
|
+
the registry-published sha512 `dist.integrity` (and legacy `dist.shasum`)
|
|
92
|
+
**before writing anything**, and treats a banner/version mismatch as fatal
|
|
93
|
+
instead of a warning ([#11](https://github.com/Guided-Rails/axe-cuprite/issues/11)).
|
|
94
|
+
- The sha512 of the vendored `axe.min.js` is now recorded in
|
|
95
|
+
`lib/axe/cuprite/vendor/axe.min.js.sha512`; a new `rake axe:verify` task
|
|
96
|
+
re-checks the vendored engine against it, and CI runs it on every build.
|
|
97
|
+
- Hardened the CI workflow: added a least-privilege `permissions: contents: read`
|
|
98
|
+
block (the `GITHUB_TOKEN` previously inherited the repo default, potentially
|
|
99
|
+
write-all) and pinned `actions/checkout` and `ruby/setup-ruby` to full commit
|
|
100
|
+
SHAs instead of mutable major-version tags. Added a Dependabot config
|
|
101
|
+
(`.github/dependabot.yml`) for the `github-actions` and `bundler` ecosystems so
|
|
102
|
+
the pinned SHAs and gem dependencies get automated update PRs
|
|
103
|
+
([#12](https://github.com/Guided-Rails/axe-cuprite/issues/12)).
|
|
104
|
+
- `rake axe:update[VERSION]` now validates the `VERSION` argument against a
|
|
105
|
+
semver-ish pattern **before** any network call or file write. The value was
|
|
106
|
+
previously spliced unvalidated into the registry/download URL and into
|
|
107
|
+
`version.rb`, so a crafted string (e.g. `4.12.0/../other-pkg`, or one
|
|
108
|
+
containing a quote) could vendor a different package or break out of the
|
|
109
|
+
version string literal ([#13](https://github.com/Guided-Rails/axe-cuprite/issues/13)).
|
|
110
|
+
|
|
10
111
|
## [0.1.1] - 2026-06-09
|
|
11
112
|
|
|
12
113
|
### Added
|
|
@@ -38,3 +139,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
38
139
|
auto-inject toggle, and report-only mode.
|
|
39
140
|
- `rake axe:update[VERSION]` to refresh the vendored axe-core engine and bump
|
|
40
141
|
the `AXE_CORE_VERSION` constant.
|
|
142
|
+
|
|
143
|
+
[Unreleased]: https://github.com/Guided-Rails/axe-cuprite/compare/v1.0.0...HEAD
|
|
144
|
+
[1.0.0]: https://github.com/Guided-Rails/axe-cuprite/compare/v0.2.0...v1.0.0
|
|
145
|
+
[0.2.0]: https://github.com/Guided-Rails/axe-cuprite/compare/v0.1.1...v0.2.0
|
|
146
|
+
[0.1.1]: https://github.com/Guided-Rails/axe-cuprite/compare/v0.1.0...v0.1.1
|
|
147
|
+
[0.1.0]: https://github.com/Guided-Rails/axe-cuprite/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# axe-cuprite
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/axe-cuprite)
|
|
4
|
+
[](https://github.com/Guided-Rails/axe-cuprite/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
|
|
3
7
|
Run the [axe-core](https://github.com/dequelabs/axe-core) accessibility engine
|
|
4
8
|
against pages in your **Capybara system/feature tests driven by
|
|
5
9
|
[Cuprite](https://github.com/rubycdp/cuprite)** (the CDP/Ferrum headless-Chrome
|
|
@@ -23,13 +27,24 @@ they break.
|
|
|
23
27
|
just a JavaScript file — a driver-agnostic engine — so we drive it exclusively
|
|
24
28
|
through Capybara's **driver-neutral** JavaScript API (`execute_script`,
|
|
25
29
|
`evaluate_async_script`), which Cuprite fully implements. That single decision is
|
|
26
|
-
what makes this gem work where the official one doesn't.
|
|
27
|
-
|
|
28
|
-
the
|
|
30
|
+
what makes this gem work where the official one doesn't.
|
|
31
|
+
|
|
32
|
+
**Cuprite is the only supported and tested driver.** Driving axe through the
|
|
33
|
+
driver-neutral JS API is what lets the gem avoid Selenium internals — not a promise
|
|
34
|
+
of broad driver support. Other real-browser drivers may happen to work (a
|
|
35
|
+
best-effort fallback exists), but they are unsupported and untested.
|
|
29
36
|
|
|
30
37
|
There is no runtime dependency on Selenium, Cuprite, or Ferrum — the only runtime
|
|
31
38
|
dependency is Capybara. You bring your own driver.
|
|
32
39
|
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- **Ruby** >= 3.0
|
|
43
|
+
- **Capybara** >= 3.0, < 4.0 (the only runtime dependency)
|
|
44
|
+
- **[Cuprite](https://github.com/rubycdp/cuprite)** as the Capybara driver — the
|
|
45
|
+
only supported and tested driver
|
|
46
|
+
- **Chrome or Chromium**, which Cuprite drives over CDP
|
|
47
|
+
|
|
33
48
|
## Installation
|
|
34
49
|
|
|
35
50
|
```ruby
|
|
@@ -146,6 +161,13 @@ on top of your configured defaults. Only `violations` and `incomplete` are
|
|
|
146
161
|
carried back across the CDP boundary — the full results object (with
|
|
147
162
|
`passes`/`inapplicable`) can be huge.
|
|
148
163
|
|
|
164
|
+
`AxeCuprite::Results` is read-only: it **takes ownership of the hash it's given
|
|
165
|
+
and deep-freezes it in place** (this backs the read-only `raw`/`to_h` contract).
|
|
166
|
+
You normally get one straight from `Runner#run`, so this is transparent — but if
|
|
167
|
+
you construct `Results.new(hash)` yourself, don't pass or keep a reference to a
|
|
168
|
+
hash you intend to mutate afterward, or you'll hit a `FrozenError`. Dup it first
|
|
169
|
+
if you need a mutable copy.
|
|
170
|
+
|
|
149
171
|
## Configuration
|
|
150
172
|
|
|
151
173
|
```ruby
|
|
@@ -156,6 +178,7 @@ AxeCuprite.configure do |c|
|
|
|
156
178
|
c.skip_rules = [:region] # globally disabled rules
|
|
157
179
|
c.auto_inject = true # (re)inject axe on demand inside #run
|
|
158
180
|
c.report_only = false # log violations instead of failing (see below)
|
|
181
|
+
c.include_html = true # include element HTML snippets in output (see below)
|
|
159
182
|
c.logger = Logger.new($stdout)
|
|
160
183
|
end
|
|
161
184
|
```
|
|
@@ -167,6 +190,22 @@ This eases incremental adoption on an existing app — you can see what axe find
|
|
|
167
190
|
without turning the suite red. Negated assertions (`expect(page).not_to
|
|
168
191
|
be_axe_clean`) ignore this flag.
|
|
169
192
|
|
|
193
|
+
### Sensitive page content in output (`include_html`)
|
|
194
|
+
|
|
195
|
+
Both failure messages and `report_only` logs include a **truncated outer-HTML
|
|
196
|
+
snippet** (capped at 200 chars) of each offending element, so you can see *what*
|
|
197
|
+
failed at a glance. On suites that render real data — staging-backed tests,
|
|
198
|
+
seeded PII — those snippets can carry page content into CI logs or log
|
|
199
|
+
aggregation. This is normal for a testing tool, but if it matters to you:
|
|
200
|
+
|
|
201
|
+
- Point `c.logger` somewhere appropriate (a redacted sink, a dropped stream)
|
|
202
|
+
rather than `$stdout`, so `report_only` output doesn't fan out to aggregation.
|
|
203
|
+
- Set `c.include_html = false` to drop the HTML snippets entirely. Output then
|
|
204
|
+
carries only the rule id, impact, help URL, element **selector**, and the axe
|
|
205
|
+
check message (for color-contrast, the fg/bg colors and ratio) — enough to
|
|
206
|
+
locate and fix a violation without echoing page content. Note the selector
|
|
207
|
+
itself can still reflect ids/classes from your markup.
|
|
208
|
+
|
|
170
209
|
## Caveats & engineering notes
|
|
171
210
|
|
|
172
211
|
### Timeout (decoupled from `default_max_wait_time`)
|
|
@@ -180,10 +219,11 @@ page routinely exceeds.
|
|
|
180
219
|
axe-cuprite avoids this trap: on Cuprite it calls Ferrum's
|
|
181
220
|
`page.evaluate_async(script, explicit_wait, *args)` **directly**, with its own
|
|
182
221
|
timeout (default **30s**, configurable) that is completely **decoupled from
|
|
183
|
-
`default_max_wait_time`**. On non-Ferrum
|
|
184
|
-
`evaluate_async_script` under a temporarily-raised wait time
|
|
185
|
-
doesn't finish, you get a
|
|
186
|
-
the timeout or scope the run
|
|
222
|
+
`default_max_wait_time`**. (On a non-Ferrum driver there is a best-effort,
|
|
223
|
+
untested fallback to `evaluate_async_script` under a temporarily-raised wait time —
|
|
224
|
+
but Cuprite is the only supported driver.) If axe still doesn't finish, you get a
|
|
225
|
+
clear `AxeCuprite::TimeoutError` telling you to raise the timeout or scope the run
|
|
226
|
+
with `.within`.
|
|
187
227
|
|
|
188
228
|
Tune it globally (`c.timeout = 60`) or per assertion (`.with_timeout(60)`).
|
|
189
229
|
|
|
@@ -223,10 +263,33 @@ To refresh it:
|
|
|
223
263
|
rake 'axe:update[4.12.0]' # pin a version
|
|
224
264
|
rake axe:update # or grab the latest from npm
|
|
225
265
|
rake axe:version # print the currently vendored version
|
|
266
|
+
rake axe:verify # check axe.min.js against its recorded sha512
|
|
226
267
|
```
|
|
227
268
|
|
|
228
|
-
`axe:update` downloads `axe
|
|
229
|
-
|
|
269
|
+
`axe:update` downloads the official `axe-core` tarball from
|
|
270
|
+
**registry.npmjs.org**, verifies it against the sha512 integrity the registry
|
|
271
|
+
publishes (aborting on any mismatch before writing a byte), extracts
|
|
272
|
+
`axe.min.js` and its `LICENSE`, and bumps the `AXE_CORE_VERSION` constant. The
|
|
273
|
+
sha512 of the vendored engine is recorded in
|
|
274
|
+
`lib/axe/cuprite/vendor/axe.min.js.sha512` so it can be re-checked at any time
|
|
275
|
+
with `rake axe:verify` (CI does this on every run). Note the bump in
|
|
276
|
+
`CHANGELOG.md`.
|
|
277
|
+
|
|
278
|
+
## Versioning
|
|
279
|
+
|
|
280
|
+
axe-cuprite follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
281
|
+
The **public API** covered by that guarantee is the documented surface:
|
|
282
|
+
`AxeCuprite` (module methods), `AxeCuprite::Configuration`, `AxeCuprite::Runner`,
|
|
283
|
+
the result wrappers (`Results` / `Violation` / `Node` / `ContrastData`), the error
|
|
284
|
+
classes, and the RSpec matcher DSL (`be_axe_clean` / `be_accessible` and its
|
|
285
|
+
chainers). Classes like `AxeCuprite::Injector` and `AxeCuprite::DeepFreeze` are
|
|
286
|
+
internal implementation details and may change in any release. Breaking changes to
|
|
287
|
+
the public API only land in a major version, and every release is noted in
|
|
288
|
+
[`CHANGELOG.md`](CHANGELOG.md).
|
|
289
|
+
|
|
290
|
+
Note that the **vendored axe-core version** can change in a minor release; axe may
|
|
291
|
+
report new violations after an engine bump, which can turn a previously-green
|
|
292
|
+
suite red. Such bumps are always called out in the changelog.
|
|
230
293
|
|
|
231
294
|
## Licensing
|
|
232
295
|
|
|
@@ -34,6 +34,13 @@ module AxeCuprite
|
|
|
34
34
|
# Logger used for report_only output and warnings.
|
|
35
35
|
attr_accessor :logger
|
|
36
36
|
|
|
37
|
+
# When true (default), failure messages and report_only logs include a
|
|
38
|
+
# truncated outer-HTML snippet of each offending element. Set to false to
|
|
39
|
+
# suppress those snippets (rule id + selector + check message only) on
|
|
40
|
+
# suites that render sensitive data, so page content can't leak into CI
|
|
41
|
+
# logs. See BeAxeClean#format_node.
|
|
42
|
+
attr_accessor :include_html
|
|
43
|
+
|
|
37
44
|
def initialize
|
|
38
45
|
@timeout = 30
|
|
39
46
|
@default_options = {}
|
|
@@ -41,6 +48,7 @@ module AxeCuprite
|
|
|
41
48
|
@skip_rules = []
|
|
42
49
|
@auto_inject = true
|
|
43
50
|
@report_only = false
|
|
51
|
+
@include_html = true
|
|
44
52
|
@logger = default_logger
|
|
45
53
|
end
|
|
46
54
|
|
data/lib/axe/cuprite/injector.rb
CHANGED
|
@@ -46,6 +46,17 @@ module AxeCuprite
|
|
|
46
46
|
# JS expression that reports whether axe is loaded and runnable.
|
|
47
47
|
PRESENCE_JS = "typeof window.axe !== 'undefined' && typeof window.axe.run === 'function'"
|
|
48
48
|
|
|
49
|
+
# Dedicated timeout classes that mean "the evaluation timed out" regardless
|
|
50
|
+
# of message. Matched by name (not constant) so we keep no hard dependency on
|
|
51
|
+
# ferrum — it's a dev-only dep. Covers Ferrum's own timeouts (the Cuprite
|
|
52
|
+
# fast-path, via page.evaluate_async). A non-Ferrum driver's own script-timeout
|
|
53
|
+
# error propagates raw rather than being wrapped — we don't depend on or test
|
|
54
|
+
# any such driver here, so we don't guess at its class names. See #timeout_error?.
|
|
55
|
+
TIMEOUT_ERROR_CLASS_NAMES = [
|
|
56
|
+
"Ferrum::TimeoutError",
|
|
57
|
+
"Ferrum::ScriptTimeoutError"
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
49
60
|
def initialize(page, configuration = AxeCuprite.configuration)
|
|
50
61
|
@page = page
|
|
51
62
|
@config = configuration
|
|
@@ -73,20 +84,23 @@ module AxeCuprite
|
|
|
73
84
|
# to Ferrum's add_script_tag.
|
|
74
85
|
def inject_source!
|
|
75
86
|
source = AxeCuprite.axe_source
|
|
87
|
+
errors = {}
|
|
76
88
|
|
|
77
89
|
begin
|
|
78
90
|
@page.execute_script(source)
|
|
79
|
-
rescue StandardError
|
|
80
|
-
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
errors["execute_script"] = e
|
|
81
93
|
end
|
|
82
94
|
return true if injected?
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
begin
|
|
97
|
+
try_add_script_tag(source)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
errors["add_script_tag"] = e
|
|
100
|
+
end
|
|
85
101
|
return true if injected?
|
|
86
102
|
|
|
87
|
-
raise InjectionError,
|
|
88
|
-
"axe-core did not load after injection. The page may be blocking " \
|
|
89
|
-
"script injection (e.g. a strict Content-Security-Policy)."
|
|
103
|
+
raise InjectionError, injection_failure_message(errors)
|
|
90
104
|
end
|
|
91
105
|
|
|
92
106
|
# Run axe and return a Results object. Injects on demand if needed.
|
|
@@ -154,24 +168,47 @@ module AxeCuprite
|
|
|
154
168
|
nil
|
|
155
169
|
end
|
|
156
170
|
|
|
157
|
-
# Best-effort CSP fallback via Ferrum's add_script_tag(content:).
|
|
171
|
+
# Best-effort CSP fallback via Ferrum's add_script_tag(content:). Returns
|
|
172
|
+
# false when no Ferrum add_script_tag is available (non-Ferrum drivers); lets
|
|
173
|
+
# a genuine injection failure propagate so inject_source! can report it.
|
|
158
174
|
def try_add_script_tag(source)
|
|
159
175
|
fpage = ferrum_page
|
|
160
176
|
return false unless fpage.respond_to?(:add_script_tag)
|
|
161
177
|
|
|
162
178
|
fpage.add_script_tag(content: source)
|
|
163
179
|
true
|
|
164
|
-
rescue StandardError
|
|
165
|
-
false
|
|
166
180
|
end
|
|
167
181
|
|
|
182
|
+
# Build the InjectionError message, appending whatever the injection paths
|
|
183
|
+
# actually raised so a non-CSP failure (dead browser, dead CDP session,
|
|
184
|
+
# misconfigured driver) isn't silently blamed on Content-Security-Policy.
|
|
185
|
+
def injection_failure_message(errors)
|
|
186
|
+
message = "axe-core did not load after injection. The page may be blocking " \
|
|
187
|
+
"script injection (e.g. a strict Content-Security-Policy)."
|
|
188
|
+
return message if errors.empty?
|
|
189
|
+
|
|
190
|
+
detail = errors.map { |path, e| "#{path}: #{e.class}: #{e.message}" }.join("; ")
|
|
191
|
+
"#{message}\nUnderlying errors: #{detail}"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Did `evaluate_axe` fail because axe.run genuinely timed out (so the
|
|
195
|
+
# "increase the timeout / scope the run" guidance is right), or did it hit a
|
|
196
|
+
# real page-side error that must propagate untouched?
|
|
197
|
+
#
|
|
198
|
+
# Classification is driven by error CLASS, not message. A message that merely
|
|
199
|
+
# mentions a timeout is deliberately NOT sufficient: a real Ferrum::JavaScriptError
|
|
200
|
+
# from axe or the app whose text happens to contain "timeout" must not be
|
|
201
|
+
# rewritten as an AxeCuprite::TimeoutError with misleading guidance.
|
|
202
|
+
#
|
|
203
|
+
# The one message check is narrow and class-gated: Ferrum reports its own
|
|
204
|
+
# async-evaluation timeout (page.evaluate_async) as a generic JavaScriptError
|
|
205
|
+
# carrying a "timed out promise" message, so for that class — and only that
|
|
206
|
+
# class — the message is what tells a timeout apart from a real JS error.
|
|
168
207
|
def timeout_error?(error)
|
|
169
|
-
|
|
208
|
+
name = error.class.name.to_s
|
|
209
|
+
return true if TIMEOUT_ERROR_CLASS_NAMES.include?(name)
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
# hard dependency on the Ferrum constants (ferrum is only a dev dep here).
|
|
173
|
-
error.class.name.to_s =~ /Ferrum::(Timeout|JavaScript)Error/ &&
|
|
174
|
-
error.message.to_s =~ /timed out promise/i
|
|
211
|
+
name == "Ferrum::JavaScriptError" && error.message.to_s.match?(/timed out promise/i)
|
|
175
212
|
end
|
|
176
213
|
end
|
|
177
214
|
end
|
data/lib/axe/cuprite/results.rb
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AxeCuprite
|
|
4
|
+
# Recursively freezes a parsed-JSON tree (hashes/arrays of scalars) so the
|
|
5
|
+
# read-only wrappers below can hand out `raw`/`to_h` without a caller being
|
|
6
|
+
# able to mutate internal state (which, via memoization, could otherwise
|
|
7
|
+
# desync `violations`/`incomplete` from `raw`). Idempotent and cheap on the
|
|
8
|
+
# slimmed payload we carry back.
|
|
9
|
+
module DeepFreeze
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(obj)
|
|
13
|
+
case obj
|
|
14
|
+
when Hash
|
|
15
|
+
obj.each { |k, v| [k, v].each { |o| call(o) } }
|
|
16
|
+
when Array
|
|
17
|
+
obj.each { |v| call(v) }
|
|
18
|
+
end
|
|
19
|
+
obj.freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
4
23
|
# Wraps the (slimmed) payload returned by axe.run. We deliberately only carry
|
|
5
24
|
# `violations` and `incomplete` across the CDP boundary plus a little metadata
|
|
6
25
|
# — the full results object (with `passes`/`inapplicable`) can be huge.
|
|
26
|
+
#
|
|
27
|
+
# The wrappers are read-only: `@raw` is deep-frozen at construction, so `raw`
|
|
28
|
+
# and `to_h` expose the live underlying hash safely (callers cannot mutate it).
|
|
29
|
+
#
|
|
30
|
+
# Ownership note: `Results` (and the nested `Violation`/`Node`/`ContrastData`
|
|
31
|
+
# wrappers) **take ownership of the hash passed in and deep-freeze it in place**
|
|
32
|
+
# — they do not copy first. For the internal flow this is safe (the payload
|
|
33
|
+
# comes fresh off the CDP boundary), but if you construct `Results.new(hash)`
|
|
34
|
+
# yourself, don't pass — or hold onto — a hash you intend to mutate afterward,
|
|
35
|
+
# or you'll hit a `FrozenError`. Dup it first if you need a mutable copy.
|
|
7
36
|
class Results
|
|
8
37
|
attr_reader :raw
|
|
9
38
|
|
|
10
39
|
def initialize(raw)
|
|
11
|
-
@raw = raw || {}
|
|
40
|
+
@raw = DeepFreeze.call(raw || {})
|
|
12
41
|
end
|
|
13
42
|
|
|
14
43
|
def violations
|
|
@@ -47,7 +76,7 @@ module AxeCuprite
|
|
|
47
76
|
attr_reader :raw
|
|
48
77
|
|
|
49
78
|
def initialize(raw)
|
|
50
|
-
@raw = raw || {}
|
|
79
|
+
@raw = DeepFreeze.call(raw || {})
|
|
51
80
|
end
|
|
52
81
|
|
|
53
82
|
def id
|
|
@@ -88,7 +117,7 @@ module AxeCuprite
|
|
|
88
117
|
attr_reader :raw
|
|
89
118
|
|
|
90
119
|
def initialize(raw)
|
|
91
|
-
@raw = raw || {}
|
|
120
|
+
@raw = DeepFreeze.call(raw || {})
|
|
92
121
|
end
|
|
93
122
|
|
|
94
123
|
# axe gives `target` as an array of CSS selectors (one per frame depth).
|
|
@@ -137,7 +166,7 @@ module AxeCuprite
|
|
|
137
166
|
attr_reader :raw
|
|
138
167
|
|
|
139
168
|
def initialize(raw)
|
|
140
|
-
@raw = raw || {}
|
|
169
|
+
@raw = DeepFreeze.call(raw || {})
|
|
141
170
|
end
|
|
142
171
|
|
|
143
172
|
def fg_color
|
|
@@ -193,7 +193,7 @@ module AxeCuprite
|
|
|
193
193
|
|
|
194
194
|
def format_node(node)
|
|
195
195
|
lines = [" - #{node.selector}"]
|
|
196
|
-
lines << " #{truncate(node.html)}" if node.html
|
|
196
|
+
lines << " #{truncate(node.html)}" if node.html && config.include_html
|
|
197
197
|
|
|
198
198
|
if (cd = node.contrast_data)
|
|
199
199
|
lines << " contrast #{cd.contrast_ratio}:1 (needs #{cd.expected_contrast_ratio}:1) — " \
|
data/lib/axe/cuprite/runner.rb
CHANGED
|
@@ -73,11 +73,7 @@ module AxeCuprite
|
|
|
73
73
|
# axe accepts a selector string, or {include:, exclude:}. Stringify hash
|
|
74
74
|
# keys so Ferrum serializes them predictably across CDP.
|
|
75
75
|
def normalize_context(context)
|
|
76
|
-
|
|
77
|
-
when nil, String then context
|
|
78
|
-
when Hash then deep_stringify(context)
|
|
79
|
-
else context
|
|
80
|
-
end
|
|
76
|
+
context.is_a?(Hash) ? deep_stringify(context) : context
|
|
81
77
|
end
|
|
82
78
|
|
|
83
79
|
def deep_stringify(value)
|
data/lib/axe/cuprite/version.rb
CHANGED
data/lib/axe/cuprite.rb
CHANGED
|
@@ -8,8 +8,10 @@ require "axe/cuprite/injector"
|
|
|
8
8
|
require "axe/cuprite/runner"
|
|
9
9
|
|
|
10
10
|
# axe-cuprite drives the axe-core accessibility engine through Capybara's
|
|
11
|
-
# driver-neutral JavaScript API
|
|
12
|
-
#
|
|
11
|
+
# driver-neutral JavaScript API. That approach is what lets it avoid Selenium
|
|
12
|
+
# internals and work on Cuprite (Ferrum/CDP) — the only supported and tested
|
|
13
|
+
# driver. Other real-browser drivers may work via a best-effort, untested
|
|
14
|
+
# fallback, but they are unsupported.
|
|
13
15
|
#
|
|
14
16
|
# Quick start:
|
|
15
17
|
#
|
data/lib/axe-cuprite.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: axe-cuprite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdullah Hashim
|
|
@@ -29,165 +29,13 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '4.0'
|
|
32
|
-
- !ruby/object:Gem::Dependency
|
|
33
|
-
name: cuprite
|
|
34
|
-
requirement: !ruby/object:Gem::Requirement
|
|
35
|
-
requirements:
|
|
36
|
-
- - "~>"
|
|
37
|
-
- !ruby/object:Gem::Version
|
|
38
|
-
version: '0.17'
|
|
39
|
-
type: :development
|
|
40
|
-
prerelease: false
|
|
41
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
-
requirements:
|
|
43
|
-
- - "~>"
|
|
44
|
-
- !ruby/object:Gem::Version
|
|
45
|
-
version: '0.17'
|
|
46
|
-
- !ruby/object:Gem::Dependency
|
|
47
|
-
name: ferrum
|
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
|
49
|
-
requirements:
|
|
50
|
-
- - "~>"
|
|
51
|
-
- !ruby/object:Gem::Version
|
|
52
|
-
version: '0.17'
|
|
53
|
-
type: :development
|
|
54
|
-
prerelease: false
|
|
55
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
-
requirements:
|
|
57
|
-
- - "~>"
|
|
58
|
-
- !ruby/object:Gem::Version
|
|
59
|
-
version: '0.17'
|
|
60
|
-
- !ruby/object:Gem::Dependency
|
|
61
|
-
name: puma
|
|
62
|
-
requirement: !ruby/object:Gem::Requirement
|
|
63
|
-
requirements:
|
|
64
|
-
- - ">="
|
|
65
|
-
- !ruby/object:Gem::Version
|
|
66
|
-
version: '5.0'
|
|
67
|
-
- - "<"
|
|
68
|
-
- !ruby/object:Gem::Version
|
|
69
|
-
version: '9.0'
|
|
70
|
-
type: :development
|
|
71
|
-
prerelease: false
|
|
72
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
73
|
-
requirements:
|
|
74
|
-
- - ">="
|
|
75
|
-
- !ruby/object:Gem::Version
|
|
76
|
-
version: '5.0'
|
|
77
|
-
- - "<"
|
|
78
|
-
- !ruby/object:Gem::Version
|
|
79
|
-
version: '9.0'
|
|
80
|
-
- !ruby/object:Gem::Dependency
|
|
81
|
-
name: rack
|
|
82
|
-
requirement: !ruby/object:Gem::Requirement
|
|
83
|
-
requirements:
|
|
84
|
-
- - ">="
|
|
85
|
-
- !ruby/object:Gem::Version
|
|
86
|
-
version: '2.0'
|
|
87
|
-
- - "<"
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '4.0'
|
|
90
|
-
type: :development
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ">="
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '2.0'
|
|
97
|
-
- - "<"
|
|
98
|
-
- !ruby/object:Gem::Version
|
|
99
|
-
version: '4.0'
|
|
100
|
-
- !ruby/object:Gem::Dependency
|
|
101
|
-
name: rackup
|
|
102
|
-
requirement: !ruby/object:Gem::Requirement
|
|
103
|
-
requirements:
|
|
104
|
-
- - "~>"
|
|
105
|
-
- !ruby/object:Gem::Version
|
|
106
|
-
version: '2.0'
|
|
107
|
-
type: :development
|
|
108
|
-
prerelease: false
|
|
109
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
110
|
-
requirements:
|
|
111
|
-
- - "~>"
|
|
112
|
-
- !ruby/object:Gem::Version
|
|
113
|
-
version: '2.0'
|
|
114
|
-
- !ruby/object:Gem::Dependency
|
|
115
|
-
name: rake
|
|
116
|
-
requirement: !ruby/object:Gem::Requirement
|
|
117
|
-
requirements:
|
|
118
|
-
- - "~>"
|
|
119
|
-
- !ruby/object:Gem::Version
|
|
120
|
-
version: '13.0'
|
|
121
|
-
type: :development
|
|
122
|
-
prerelease: false
|
|
123
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
124
|
-
requirements:
|
|
125
|
-
- - "~>"
|
|
126
|
-
- !ruby/object:Gem::Version
|
|
127
|
-
version: '13.0'
|
|
128
|
-
- !ruby/object:Gem::Dependency
|
|
129
|
-
name: rspec
|
|
130
|
-
requirement: !ruby/object:Gem::Requirement
|
|
131
|
-
requirements:
|
|
132
|
-
- - "~>"
|
|
133
|
-
- !ruby/object:Gem::Version
|
|
134
|
-
version: '3.0'
|
|
135
|
-
type: :development
|
|
136
|
-
prerelease: false
|
|
137
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
138
|
-
requirements:
|
|
139
|
-
- - "~>"
|
|
140
|
-
- !ruby/object:Gem::Version
|
|
141
|
-
version: '3.0'
|
|
142
|
-
- !ruby/object:Gem::Dependency
|
|
143
|
-
name: rubocop
|
|
144
|
-
requirement: !ruby/object:Gem::Requirement
|
|
145
|
-
requirements:
|
|
146
|
-
- - "~>"
|
|
147
|
-
- !ruby/object:Gem::Version
|
|
148
|
-
version: '1.86'
|
|
149
|
-
type: :development
|
|
150
|
-
prerelease: false
|
|
151
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
152
|
-
requirements:
|
|
153
|
-
- - "~>"
|
|
154
|
-
- !ruby/object:Gem::Version
|
|
155
|
-
version: '1.86'
|
|
156
|
-
- !ruby/object:Gem::Dependency
|
|
157
|
-
name: rubocop-rake
|
|
158
|
-
requirement: !ruby/object:Gem::Requirement
|
|
159
|
-
requirements:
|
|
160
|
-
- - "~>"
|
|
161
|
-
- !ruby/object:Gem::Version
|
|
162
|
-
version: '0.7'
|
|
163
|
-
type: :development
|
|
164
|
-
prerelease: false
|
|
165
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
166
|
-
requirements:
|
|
167
|
-
- - "~>"
|
|
168
|
-
- !ruby/object:Gem::Version
|
|
169
|
-
version: '0.7'
|
|
170
|
-
- !ruby/object:Gem::Dependency
|
|
171
|
-
name: rubocop-rspec
|
|
172
|
-
requirement: !ruby/object:Gem::Requirement
|
|
173
|
-
requirements:
|
|
174
|
-
- - "~>"
|
|
175
|
-
- !ruby/object:Gem::Version
|
|
176
|
-
version: '3.0'
|
|
177
|
-
type: :development
|
|
178
|
-
prerelease: false
|
|
179
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
180
|
-
requirements:
|
|
181
|
-
- - "~>"
|
|
182
|
-
- !ruby/object:Gem::Version
|
|
183
|
-
version: '3.0'
|
|
184
32
|
description: |
|
|
185
33
|
axe-cuprite runs the axe-core accessibility engine against pages rendered in
|
|
186
34
|
Capybara system/feature tests and exposes the results as RSpec matchers
|
|
187
35
|
(be_axe_clean / be_accessible). Unlike Deque's official axe-core-capybara gem,
|
|
188
36
|
it never touches Selenium-specific driver internals: axe is driven entirely
|
|
189
|
-
through Capybara's driver-neutral JavaScript API,
|
|
190
|
-
|
|
37
|
+
through Capybara's driver-neutral JavaScript API, which is what makes it work
|
|
38
|
+
on Cuprite. Cuprite is the only supported and tested driver.
|
|
191
39
|
email:
|
|
192
40
|
- abdullah@guidedrails.com
|
|
193
41
|
executables: []
|
|
@@ -207,7 +55,6 @@ files:
|
|
|
207
55
|
- lib/axe/cuprite/rspec.rb
|
|
208
56
|
- lib/axe/cuprite/rspec/matchers.rb
|
|
209
57
|
- lib/axe/cuprite/runner.rb
|
|
210
|
-
- lib/axe/cuprite/tasks/axe.rake
|
|
211
58
|
- lib/axe/cuprite/vendor/axe-core-LICENSE.txt
|
|
212
59
|
- lib/axe/cuprite/vendor/axe.min.js
|
|
213
60
|
- lib/axe/cuprite/version.rb
|
|
@@ -217,6 +64,8 @@ licenses:
|
|
|
217
64
|
metadata:
|
|
218
65
|
source_code_uri: https://github.com/Guided-Rails/axe-cuprite
|
|
219
66
|
changelog_uri: https://github.com/Guided-Rails/axe-cuprite/blob/main/CHANGELOG.md
|
|
67
|
+
bug_tracker_uri: https://github.com/Guided-Rails/axe-cuprite/issues
|
|
68
|
+
documentation_uri: https://github.com/Guided-Rails/axe-cuprite/blob/main/README.md
|
|
220
69
|
rubygems_mfa_required: 'true'
|
|
221
70
|
rdoc_options: []
|
|
222
71
|
require_paths:
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open-uri"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
|
-
namespace :axe do
|
|
8
|
-
desc "Refresh the vendored axe-core engine. Usage: rake 'axe:update[4.12.0]' or VERSION=4.12.0 rake axe:update (default: latest)"
|
|
9
|
-
task :update, [:version] do |_task, args|
|
|
10
|
-
version = args[:version] || ENV["VERSION"] || AxeCupriteVendor.latest_version
|
|
11
|
-
AxeCupriteVendor.update!(version)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
desc "Print the currently vendored axe-core version"
|
|
15
|
-
task :version do
|
|
16
|
-
puts AxeCupriteVendor.vendored_version
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Helpers for vendoring axe-core. Kept in a module so the rake tasks stay thin
|
|
21
|
-
# and the logic is easy to read. Never used at runtime — vendoring is a
|
|
22
|
-
# development-time step; the engine ships in the gem.
|
|
23
|
-
module AxeCupriteVendor
|
|
24
|
-
VENDOR_DIR = File.expand_path("../vendor", __dir__)
|
|
25
|
-
AXE_JS = File.join(VENDOR_DIR, "axe.min.js")
|
|
26
|
-
AXE_LICENSE = File.join(VENDOR_DIR, "axe-core-LICENSE.txt")
|
|
27
|
-
VERSION_FILE = File.expand_path("../version.rb", __dir__)
|
|
28
|
-
REGISTRY = "https://registry.npmjs.org/axe-core"
|
|
29
|
-
CDN = "https://unpkg.com/axe-core"
|
|
30
|
-
|
|
31
|
-
module_function
|
|
32
|
-
|
|
33
|
-
def update!(version)
|
|
34
|
-
FileUtils.mkdir_p(VENDOR_DIR)
|
|
35
|
-
|
|
36
|
-
puts "Fetching axe-core@#{version} ..."
|
|
37
|
-
js = download("#{CDN}@#{version}/axe.min.js")
|
|
38
|
-
unless js =~ /axe v#{Regexp.escape(version)}\b/
|
|
39
|
-
warn "Warning: downloaded axe.min.js banner does not mention v#{version} — continuing anyway."
|
|
40
|
-
end
|
|
41
|
-
license = download("#{CDN}@#{version}/LICENSE")
|
|
42
|
-
|
|
43
|
-
File.write(AXE_JS, js)
|
|
44
|
-
File.write(AXE_LICENSE, license)
|
|
45
|
-
bump_version_constant(version)
|
|
46
|
-
|
|
47
|
-
puts "Vendored axe-core #{version}:"
|
|
48
|
-
puts " #{AXE_JS} (#{js.bytesize} bytes)"
|
|
49
|
-
puts " #{AXE_LICENSE}"
|
|
50
|
-
puts " updated AXE_CORE_VERSION in #{VERSION_FILE}"
|
|
51
|
-
puts "Remember to note the version bump in CHANGELOG.md."
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def latest_version
|
|
55
|
-
JSON.parse(download("#{REGISTRY}/latest")).fetch("version")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def vendored_version
|
|
59
|
-
File.read(AXE_JS)[/axe v([0-9][0-9.]*)/, 1] || "unknown"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def download(url)
|
|
63
|
-
URI.parse(url).open(&:read)
|
|
64
|
-
rescue OpenURI::HTTPError => e
|
|
65
|
-
abort "Failed to download #{url}: #{e.message}"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def bump_version_constant(version)
|
|
69
|
-
contents = File.read(VERSION_FILE)
|
|
70
|
-
updated = contents.sub(/(AXE_CORE_VERSION\s*=\s*)"[^"]*"/, %(\\1"#{version}"))
|
|
71
|
-
File.write(VERSION_FILE, updated)
|
|
72
|
-
end
|
|
73
|
-
end
|