Accessibility Testing with Selenium WebDriver & Axe Core
This skill enables automated accessibility analysis within the Selenium WebDriver framework using the axe-core engine to detect WCAG violations and best practice issues directly in the browser.
Activation: This skill is triggered when you need to validate WCAG compliance, scan for accessibility violations, test keyboard navigation, audit ARIA semantics, or generate a11y reports.
First Questions to Ask
- What app URL(s) or user flows are in scope (and what is explicitly out of scope)?
- Is there an existing Selenium setup and how is CI run?
- Which standard is the target (WCAG 2.1 AA by default), and are there org-specific policies?
- Which pages/components are highest risk (auth, checkout, forms, modals, navigation)?
- Are there known constraints (legacy markup, third-party widgets) that require exceptions?
Prerequisites
| Component |
Version |
Purpose |
| Java JDK |
21+ |
Runtime with modern features |
| Maven |
3.9+ |
Dependency management |
| Selenium WebDriver |
4.x |
Browser automation |
| axe-core-selenium |
4.10+ |
Deque axe-core integration |
| JUnit 5 |
5.10+ |
Test framework |
| AssertJ |
3.x |
Fluent assertions for readable failures |
| Allure |
2.x |
Reporting with a11y violation attachments |
Note: Use com.deque.html.axe-core:selenium Maven dependency for axe integration.
WCAG Compliance Levels
| Level |
Requirement |
Legal Status |
Axe Tags |
| Level A |
Basic accessibility (must have) |
Minimum legal requirement |
wcag2a, wcag21a |
| Level AA |
Intermediate (should have) |
Legal requirement in most jurisdictions |
wcag2aa, wcag21aa |
| Level AAA |
Advanced (nice to have) |
Not typically required |
wcag2aaa, wcag21aaa |
| Best Practice |
Industry recommendations |
Not WCAG but improves UX |
best-practice |
Axe-Core Tools Reference
AxeBuilder Configuration
| Method |
Purpose |
Example |
new AxeBuilder() |
Create scanner instance |
Entry point |
.withTags(List<String>) |
Filter by WCAG tags |
wcag2aa, wcag21aa |
.include(String) |
Scan specific selector |
#main-content |
.exclude(String) |
Skip selector from scan |
.third-party-widget |
.disableRules(List<String>) |
Disable specific rules |
color-contrast |
.withRules(List<String>) |
Run only specific rules |
label, button-name |
.analyze(WebDriver) |
Execute the scan |
Returns Results |
Results Object
| Method |
Returns |
Purpose |
getViolations() |
List<Rule> |
Rules that failed |
getPasses() |
List<Rule> |
Rules that passed |
getIncomplete() |
List<Rule> |
Rules needing manual review |
getInapplicable() |
List<Rule> |
Rules not applicable to page |
violationFree() |
boolean |
True if no violations |
Violation Impact Levels
| Impact |
Severity |
CI Action |
| Critical |
Blocks users completely |
Always fail build |
| Serious |
Significant barrier |
Always fail build |
| Moderate |
Some difficulty |
Warn or fail |
| Minor |
Inconvenience |
Log for review |
Core Capabilities
1. Axe Builder Analysis
- Full Page Scan:
new AxeBuilder().analyze(driver)
- Component Scan:
new AxeBuilder().include("#my-component").analyze(driver)
- Rule Configuration:
.withTags(List.of("wcag2a", "wcag2aa"))
- Exclusions:
.exclude(".legacy-footer") (use carefully, document reason)
2. Validation & Assertion
- Analyze
Results.getViolations() - should be empty
- Filter by impact level (Critical, Serious, Moderate, Minor)
- Use AssertJ Soft Assertions to report all violations before failing
3. Reporting
- Log: Rule ID + Help URL + Selector for each violation
- Serialize
Results to JSON for dashboards
- Attach to Allure reports
Your Role
As an Accessibility Automation Specialist:
- Integration: Configure axe-core with Selenium WebDriver
- Configuration: Set up
AxeBuilder with appropriate WCAG tags
- Analysis: Parse results to identify violations by impact
- Assertion: Fail on Critical/Serious, warn on Moderate/Minor
- Reporting: Log Help URLs and selectors for remediation
Step-by-Step Workflows
Workflow 1: Add A11y Scan to Existing Test
Add dependency to pom.xml
<dependency>
<groupId>com.deque.html.axe-core</groupId>
<artifactId>selenium</artifactId>
<version>4.10.0</version>
</dependency>
Create AccessibilityHelper utility
Add scan after page loads
driver.get("https://example.com");
waitForPageReady();
AccessibilityHelper.verifyPageAccessibility(driver);
Run and review violations
mvn test -Dtest=A11yTest
Workflow 2: Test Specific Component
Navigate to page with component visible
Trigger component state (open modal, show dropdown)
Scan only the component
Results results = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa"))
.include("#login-modal")
.analyze(driver);
Assert and log
Workflow 3: Keyboard Navigation Audit
- Identify all interactive elements
- Tab through the page programmatically
element.sendKeys(Keys.TAB);
WebElement focused = driver.switchTo().activeElement();
- Verify focus order is logical
- Test Escape closes modals
- Verify no keyboard traps
Workflow 4: CI Integration
Configure headless browser
mvn test -Dheadless=true -Dgroups=a11y
Set zero-tolerance for Critical/Serious
long criticalCount = violations.stream()
.filter(v -> List.of("critical", "serious").contains(v.getImpact()))
.count();
assertThat(criticalCount).isZero();
Generate JSON report for tracking
Code Patterns
Basic Full-Page Scan
@Step("Verify page accessibility - WCAG 2.1 AA")
public void verifyPageAccessibility(WebDriver driver) {
Results results = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
.analyze(driver);
logViolations(results.getViolations());
assertThat(results.violationFree())
.as("Accessibility violations found on: %s", driver.getCurrentUrl())
.isTrue();
}
Component-Specific Scan
@Step("Verify component accessibility: {selectors}")
public void verifyComponentAccessibility(WebDriver driver, String... selectors) {
AxeBuilder builder = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa"));
for (String selector : selectors) {
builder.include(selector);
}
Results results = builder.analyze(driver);
logViolations(results.getViolations());
assertThat(results.violationFree())
.as("Component accessibility check failed")
.isTrue();
}
Filter by Impact Level
@Step("Verify no critical accessibility violations")
public void verifyCriticalViolations(WebDriver driver) {
Results results = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa"))
.analyze(driver);
List<Rule> criticalViolations = results.getViolations().stream()
.filter(v -> List.of("critical", "serious").contains(v.getImpact()))
.toList();
if (!criticalViolations.isEmpty()) {
logViolations(criticalViolations);
}
assertThat(criticalViolations)
.as("Critical/Serious accessibility violations found")
.isEmpty();
}
With Documented Exclusions
/**
* Scan with exclusions for known issues.
* Exclusions must be documented with ticket reference.
*/
@Step("Verify accessibility with documented exclusions")
public void verifyWithExclusions(WebDriver driver) {
Results results = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa"))
.exclude(".third-party-chat-widget") // JIRA-1234: Vendor limitation
.exclude("#legacy-footer") // JIRA-5678: Scheduled for Q2 fix
.analyze(driver);
assertThat(results.violationFree()).isTrue();
}
Violation Logger
private void logViolations(List<Rule> violations) {
if (violations.isEmpty()) {
log.info("✓ No accessibility violations found");
return;
}
log.error("✗ Found {} accessibility violations:", violations.size());
for (Rule violation : violations) {
log.error(" [{}/{}] {}",
violation.getImpact().toUpperCase(),
violation.getId(),
violation.getDescription());
log.error(" Help: {}", violation.getHelpUrl());
for (CheckedNode node : violation.getNodes()) {
log.error(" Target: {}", String.join(", ", node.getTarget()));
log.error(" HTML: {}", truncate(node.getHtml(), 100));
}
}
}
JUnit 5 Test Class
@Epic("Accessibility")
@Feature("WCAG 2.1 AA Compliance")
class AccessibilityTest extends BaseTest {
@Test
@Tag("a11y")
@Severity(SeverityLevel.CRITICAL)
@DisplayName("Homepage should meet WCAG 2.1 AA standards")
void homePage_shouldBeAccessible() {
driver.get(ConfigReader.get("base.url"));
waitForPageReady();
Results results = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
.analyze(driver);
attachResultsToAllure(results);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(results.violationFree())
.as("Page should have no accessibility violations")
.isTrue();
});
}
@Test
@Tag("a11y")
@DisplayName("Login modal should be keyboard accessible")
void loginModal_shouldBeKeyboardAccessible() {
driver.get(ConfigReader.get("base.url"));
// Open modal
driver.findElement(By.id("login-btn")).click();
waitForVisible(By.id("login-modal"));
// Scan modal only
Results results = new AxeBuilder()
.withTags(List.of("wcag2a", "wcag2aa"))
.include("#login-modal")
.analyze(driver);
assertThat(results.violationFree()).isTrue();
// Test keyboard navigation
WebElement modal = driver.findElement(By.id("login-modal"));
WebElement firstInput = modal.findElement(By.cssSelector("input:first-of-type"));
assertThat(driver.switchTo().activeElement())
.as("Focus should be inside modal")
.isEqualTo(firstInput);
// Test Escape closes modal
modal.sendKeys(Keys.ESCAPE);
assertThat(isDisplayed(By.id("login-modal"))).isFalse();
}
}
Troubleshooting
| Problem |
Cause |
Solution |
| Axe returns empty results |
Page not fully loaded |
Add explicit wait for page ready state |
| False positives on contrast |
Dynamic themes |
Test both light and dark modes |
| Violations in third-party widgets |
Cannot modify vendor code |
Use .exclude() with documented ticket |
| Incomplete rules |
Requires manual review |
Log for manual audit, don't auto-fail |
| Different results between runs |
Async content loading |
Ensure deterministic page state before scan |
| CI fails but local passes |
Different viewport/browser |
Use same headless config as CI |
Best Practices Checklist
✅ Wait for page ready - Ensure DOM is stable before axe analysis
✅ Scan unique states - Test modal open, form error, empty state separately
✅ Zero tolerance for Critical/Serious - Always fail CI on these
✅ Use specific tags - Define wcag2aa vs best-practice to reduce noise
✅ Log Help URLs - Developers need the link to fix issues
✅ Document exclusions - Every .exclude() needs a JIRA ticket
✅ Test keyboard navigation - Tab order, focus traps, Escape key
✅ Attach JSON reports - Enable tracking violations over time
✅ Combine with manual audit - Axe catches ~30-50% of issues
Guardrails (Important Limitations)
⚠️ Automated tooling cannot prove full WCAG conformance - only the presence of certain issues
⚠️ Use automation to prevent regressions - use manual audits for complete coverage
⚠️ Prefer native HTML semantics - use ARIA only when required
⚠️ Never disable rules globally - scope exceptions narrowly with documentation
Triage by POUR Principles
| Principle |
Focus Areas |
Common Violations |
| Perceivable |
Text alternatives, captions, contrast, structure |
Missing alt text, low contrast, missing labels |
| Operable |
Keyboard access, focus order, bypass blocks |
Keyboard traps, no skip link, focus not visible |
| Understandable |
Labels, predictable behavior, error handling |
Unclear instructions, unexpected changes |
| Robust |
Valid HTML, ARIA, name/role/value |
Invalid ARIA, duplicate IDs, missing roles |
Running Tests
Maven Commands
| Command |
Purpose |
mvn test -Dgroups=a11y |
Run all accessibility tests |
mvn test -Dtest=A11yTest |
Run specific test class |
mvn test -Dheadless=true |
Run headless (CI mode) |
mvn allure:serve |
View Allure report with violations |
CI/CD Integration
- name: Run Accessibility Tests
run: mvn test -Dgroups=a11y -Dheadless=true
- name: Upload A11y Report
uses: actions/upload-artifact@v3
with:
name: a11y-report
path: target/a11y-results/
Common Rationalizations
Common shortcuts and "good enough" excuses that erode test quality — and the reality behind each.
| Rationalization |
Reality |
| "Selenium isn't good for a11y testing" |
axe-core + Selenium is battle-tested, CI-ready, and covers WCAG violations programmatically. |
| "We can just run a scan at the end" |
Shift-left: catch violations as code is written. Late scans mean expensive fixes. |
| "The framework handles accessibility" |
No framework auto-generates proper ARIA roles, labels, or keyboard interactions. |
| "We only need to test the homepage" |
Every page a user visits must be accessible. Start with high-risk pages, expand coverage. |
| "Skip the contrast checks, designers fix that" |
Automated contrast checks take seconds and prevent lawsuits. They are tests, not design reviews. |
| "Our users don't have disabilities" |
~15% of the global population has some form of disability. Accessibility is for everyone. |
References
Quick Reference
| Task |
Code Pattern |
| Full page scan |
new AxeBuilder().withTags(List.of("wcag2aa")).analyze(driver) |
| Component scan |
new AxeBuilder().include("#selector").analyze(driver) |
| Exclude element |
new AxeBuilder().exclude(".ignore").analyze(driver) |
| Check violations |
results.getViolations().isEmpty() |
| Filter critical |
.filter(v -> v.getImpact().equals("critical")) |
| Get help URL |
violation.getHelpUrl() |
| Tab navigation |
element.sendKeys(Keys.TAB) |
| Get focused element |
driver.switchTo().activeElement() |
Verification
After completing this skill's workflow, confirm:
1---2name: accessibility-selenium-testing3description: Accessibility testing toolkit using Selenium WebDriver 4+ with Java 21+ and axe-core engine. Use when asked to validate WCAG 2.1/2.2 compliance, scan pages or components for a11y violations, test keyboard navigation, audit color contrast, check ARIA semantics, generate accessibility reports, filter axe rules, debug screen reader issues, or implement POUR principles (perceivable, operable, understandable, robust).4---5
6# Accessibility Testing with Selenium WebDriver & Axe Core
7
8This skill enables automated accessibility analysis within the Selenium WebDriver framework using the **axe-core** engine to detect WCAG violations and best practice issues directly in the browser.
9
10> **Activation:** This skill is triggered when you need to validate WCAG compliance, scan for accessibility violations, test keyboard navigation, audit ARIA semantics, or generate a11y reports.
11
12## First Questions to Ask
13
14- What app URL(s) or user flows are in scope (and what is explicitly out of scope)?
15- Is there an existing Selenium setup and how is CI run?
16- Which standard is the target (WCAG 2.1 AA by default), and are there org-specific policies?
17- Which pages/components are highest risk (auth, checkout, forms, modals, navigation)?
18- Are there known constraints (legacy markup, third-party widgets) that require exceptions?
19
20## Prerequisites
21
22| Component | Version | Purpose |
23|-----------|---------|---------|
24| Java JDK | 21+ | Runtime with modern features |
25| Maven | 3.9+ | Dependency management |
26| Selenium WebDriver | 4.x | Browser automation |
27| axe-core-selenium | 4.10+ | Deque axe-core integration |
28| JUnit 5 | 5.10+ | Test framework |
29| AssertJ | 3.x | Fluent assertions for readable failures |
30| Allure | 2.x | Reporting with a11y violation attachments |
31
32> **Note:** Use `com.deque.html.axe-core:selenium` Maven dependency for axe integration.
33
34---
35
36## WCAG Compliance Levels
37
38| Level | Requirement | Legal Status | Axe Tags |
39|-------|-------------|--------------|----------|
40| **Level A** | Basic accessibility (must have) | Minimum legal requirement | `wcag2a`, `wcag21a` |
41| **Level AA** | Intermediate (should have) | Legal requirement in most jurisdictions | `wcag2aa`, `wcag21aa` |
42| **Level AAA** | Advanced (nice to have) | Not typically required | `wcag2aaa`, `wcag21aaa` |
43| **Best Practice** | Industry recommendations | Not WCAG but improves UX | `best-practice` |
44
45---
46
47## Axe-Core Tools Reference
48
49### AxeBuilder Configuration
50
51| Method | Purpose | Example |
52|--------|---------|---------|
53| `new AxeBuilder()` | Create scanner instance | Entry point |
54| `.withTags(List<String>)` | Filter by WCAG tags | `wcag2aa`, `wcag21aa` |
55| `.include(String)` | Scan specific selector | `#main-content` |
56| `.exclude(String)` | Skip selector from scan | `.third-party-widget` |
57| `.disableRules(List<String>)` | Disable specific rules | `color-contrast` |
58| `.withRules(List<String>)` | Run only specific rules | `label`, `button-name` |
59| `.analyze(WebDriver)` | Execute the scan | Returns `Results` |
60
61### Results Object
62
63| Method | Returns | Purpose |
64|--------|---------|---------|
65| `getViolations()` | `List<Rule>` | Rules that failed |
66| `getPasses()` | `List<Rule>` | Rules that passed |
67| `getIncomplete()` | `List<Rule>` | Rules needing manual review |
68| `getInapplicable()` | `List<Rule>` | Rules not applicable to page |
69| `violationFree()` | `boolean` | True if no violations |
70
71### Violation Impact Levels
72
73| Impact | Severity | CI Action |
74|--------|----------|-----------|
75| **Critical** | Blocks users completely | Always fail build |
76| **Serious** | Significant barrier | Always fail build |
77| **Moderate** | Some difficulty | Warn or fail |
78| **Minor** | Inconvenience | Log for review |
79
80---
81
82## Core Capabilities
83
84### 1. Axe Builder Analysis
85- **Full Page Scan**: `new AxeBuilder().analyze(driver)`
86- **Component Scan**: `new AxeBuilder().include("#my-component").analyze(driver)`
87- **Rule Configuration**: `.withTags(List.of("wcag2a", "wcag2aa"))`
88- **Exclusions**: `.exclude(".legacy-footer")` (use carefully, document reason)
89
90### 2. Validation & Assertion
91- Analyze `Results.getViolations()` - should be empty
92- Filter by impact level (Critical, Serious, Moderate, Minor)
93- Use AssertJ Soft Assertions to report all violations before failing
94
95### 3. Reporting
96- Log: Rule ID + Help URL + Selector for each violation
97- Serialize `Results` to JSON for dashboards
98- Attach to Allure reports
99
100---
101
102## Your Role
103
104As an Accessibility Automation Specialist:
105
1061. **Integration**: Configure axe-core with Selenium WebDriver
1072. **Configuration**: Set up `AxeBuilder` with appropriate WCAG tags
1083. **Analysis**: Parse results to identify violations by impact
1094. **Assertion**: Fail on Critical/Serious, warn on Moderate/Minor
1105. **Reporting**: Log Help URLs and selectors for remediation
111
112---
113
114## Step-by-Step Workflows
115
116### Workflow 1: Add A11y Scan to Existing Test
117
1181. **Add dependency to pom.xml**
119 ```xml
120 <dependency>
121 <groupId>com.deque.html.axe-core</groupId>
122 <artifactId>selenium</artifactId>
123 <version>4.10.0</version>
124 </dependency>
125 ```
126
1272. **Create AccessibilityHelper utility**
128 - See [Axe Patterns Guide](references/axe_patterns.md)
129
1303. **Add scan after page loads**
131 ```java
132 driver.get("https://example.com");
133 waitForPageReady();
134 AccessibilityHelper.verifyPageAccessibility(driver);
135 ```
136
1374. **Run and review violations**
138 ```bash
139 mvn test -Dtest=A11yTest
140 ```
141
142### Workflow 2: Test Specific Component
143
1441. **Navigate to page with component visible**
1452. **Trigger component state** (open modal, show dropdown)
1463. **Scan only the component**
147 ```java
148 Results results = new AxeBuilder()
149 .withTags(List.of("wcag2a", "wcag2aa"))
150 .include("#login-modal")
151 .analyze(driver);
152 ```
153
1544. **Assert and log**
155
156### Workflow 3: Keyboard Navigation Audit
157
1581. **Identify all interactive elements**
1592. **Tab through the page programmatically**
160 ```java
161 element.sendKeys(Keys.TAB);
162 WebElement focused = driver.switchTo().activeElement();
163 ```
1643. **Verify focus order is logical**
1654. **Test Escape closes modals**
1665. **Verify no keyboard traps**
167
168### Workflow 4: CI Integration
169
1701. **Configure headless browser**
171 ```bash
172 mvn test -Dheadless=true -Dgroups=a11y
173 ```
174
1752. **Set zero-tolerance for Critical/Serious**
176 ```java
177 long criticalCount = violations.stream()
178 .filter(v -> List.of("critical", "serious").contains(v.getImpact()))
179 .count();
180 assertThat(criticalCount).isZero();
181 ```
182
1833. **Generate JSON report for tracking**
184
185---
186
187## Code Patterns
188
189### Basic Full-Page Scan
190
191```java
192@Step("Verify page accessibility - WCAG 2.1 AA")
193public void verifyPageAccessibility(WebDriver driver) {
194 Results results = new AxeBuilder()
195 .withTags(List.of("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
196 .analyze(driver);
197
198 logViolations(results.getViolations());
199
200 assertThat(results.violationFree())
201 .as("Accessibility violations found on: %s", driver.getCurrentUrl())
202 .isTrue();
203}
204```
205
206### Component-Specific Scan
207
208```java
209@Step("Verify component accessibility: {selectors}")
210public void verifyComponentAccessibility(WebDriver driver, String... selectors) {
211 AxeBuilder builder = new AxeBuilder()
212 .withTags(List.of("wcag2a", "wcag2aa"));
213
214 for (String selector : selectors) {
215 builder.include(selector);
216 }
217
218 Results results = builder.analyze(driver);
219 logViolations(results.getViolations());
220
221 assertThat(results.violationFree())
222 .as("Component accessibility check failed")
223 .isTrue();
224}
225```
226
227### Filter by Impact Level
228
229```java
230@Step("Verify no critical accessibility violations")
231public void verifyCriticalViolations(WebDriver driver) {
232 Results results = new AxeBuilder()
233 .withTags(List.of("wcag2a", "wcag2aa"))
234 .analyze(driver);
235
236 List<Rule> criticalViolations = results.getViolations().stream()
237 .filter(v -> List.of("critical", "serious").contains(v.getImpact()))
238 .toList();
239
240 if (!criticalViolations.isEmpty()) {
241 logViolations(criticalViolations);
242 }
243
244 assertThat(criticalViolations)
245 .as("Critical/Serious accessibility violations found")
246 .isEmpty();
247}
248```
249
250### With Documented Exclusions
251
252```java
253/**
254 * Scan with exclusions for known issues.
255 * Exclusions must be documented with ticket reference.
256 */
257@Step("Verify accessibility with documented exclusions")
258public void verifyWithExclusions(WebDriver driver) {
259 Results results = new AxeBuilder()
260 .withTags(List.of("wcag2a", "wcag2aa"))
261 .exclude(".third-party-chat-widget") // JIRA-1234: Vendor limitation
262 .exclude("#legacy-footer") // JIRA-5678: Scheduled for Q2 fix
263 .analyze(driver);
264
265 assertThat(results.violationFree()).isTrue();
266}
267```
268
269### Violation Logger
270
271```java
272private void logViolations(List<Rule> violations) {
273 if (violations.isEmpty()) {
274 log.info("✓ No accessibility violations found");
275 return;
276 }
277
278 log.error("✗ Found {} accessibility violations:", violations.size());
279 for (Rule violation : violations) {
280 log.error(" [{}/{}] {}",
281 violation.getImpact().toUpperCase(),
282 violation.getId(),
283 violation.getDescription());
284 log.error(" Help: {}", violation.getHelpUrl());
285
286 for (CheckedNode node : violation.getNodes()) {
287 log.error(" Target: {}", String.join(", ", node.getTarget()));
288 log.error(" HTML: {}", truncate(node.getHtml(), 100));
289 }
290 }
291}
292```
293
294### JUnit 5 Test Class
295
296```java
297@Epic("Accessibility")
298@Feature("WCAG 2.1 AA Compliance")
299class AccessibilityTest extends BaseTest {
300
301 @Test
302 @Tag("a11y")
303 @Severity(SeverityLevel.CRITICAL)
304 @DisplayName("Homepage should meet WCAG 2.1 AA standards")
305 void homePage_shouldBeAccessible() {
306 driver.get(ConfigReader.get("base.url"));
307 waitForPageReady();
308
309 Results results = new AxeBuilder()
310 .withTags(List.of("wcag2a", "wcag2aa", "wcag21a", "wcag21aa"))
311 .analyze(driver);
312
313 attachResultsToAllure(results);
314
315 SoftAssertions.assertSoftly(softly -> {
316 softly.assertThat(results.violationFree())
317 .as("Page should have no accessibility violations")
318 .isTrue();
319 });
320 }
321
322 @Test
323 @Tag("a11y")
324 @DisplayName("Login modal should be keyboard accessible")
325 void loginModal_shouldBeKeyboardAccessible() {
326 driver.get(ConfigReader.get("base.url"));
327
328 // Open modal
329 driver.findElement(By.id("login-btn")).click();
330 waitForVisible(By.id("login-modal"));
331
332 // Scan modal only
333 Results results = new AxeBuilder()
334 .withTags(List.of("wcag2a", "wcag2aa"))
335 .include("#login-modal")
336 .analyze(driver);
337
338 assertThat(results.violationFree()).isTrue();
339
340 // Test keyboard navigation
341 WebElement modal = driver.findElement(By.id("login-modal"));
342 WebElement firstInput = modal.findElement(By.cssSelector("input:first-of-type"));
343
344 assertThat(driver.switchTo().activeElement())
345 .as("Focus should be inside modal")
346 .isEqualTo(firstInput);
347
348 // Test Escape closes modal
349 modal.sendKeys(Keys.ESCAPE);
350 assertThat(isDisplayed(By.id("login-modal"))).isFalse();
351 }
352}
353```
354
355---
356
357## Troubleshooting
358
359| Problem | Cause | Solution |
360|---------|-------|----------|
361| Axe returns empty results | Page not fully loaded | Add explicit wait for page ready state |
362| False positives on contrast | Dynamic themes | Test both light and dark modes |
363| Violations in third-party widgets | Cannot modify vendor code | Use `.exclude()` with documented ticket |
364| Incomplete rules | Requires manual review | Log for manual audit, don't auto-fail |
365| Different results between runs | Async content loading | Ensure deterministic page state before scan |
366| CI fails but local passes | Different viewport/browser | Use same headless config as CI |
367
368---
369
370## Best Practices Checklist
371
372✅ **Wait for page ready** - Ensure DOM is stable before axe analysis
373✅ **Scan unique states** - Test modal open, form error, empty state separately
374✅ **Zero tolerance for Critical/Serious** - Always fail CI on these
375✅ **Use specific tags** - Define `wcag2aa` vs `best-practice` to reduce noise
376✅ **Log Help URLs** - Developers need the link to fix issues
377✅ **Document exclusions** - Every `.exclude()` needs a JIRA ticket
378✅ **Test keyboard navigation** - Tab order, focus traps, Escape key
379✅ **Attach JSON reports** - Enable tracking violations over time
380✅ **Combine with manual audit** - Axe catches ~30-50% of issues
381
382---
383
384## Guardrails (Important Limitations)
385
386⚠️ **Automated tooling cannot prove full WCAG conformance** - only the presence of certain issues
387⚠️ **Use automation to prevent regressions** - use manual audits for complete coverage
388⚠️ **Prefer native HTML semantics** - use ARIA only when required
389⚠️ **Never disable rules globally** - scope exceptions narrowly with documentation
390
391---
392
393## Triage by POUR Principles
394
395| Principle | Focus Areas | Common Violations |
396|-----------|-------------|-------------------|
397| **Perceivable** | Text alternatives, captions, contrast, structure | Missing alt text, low contrast, missing labels |
398| **Operable** | Keyboard access, focus order, bypass blocks | Keyboard traps, no skip link, focus not visible |
399| **Understandable** | Labels, predictable behavior, error handling | Unclear instructions, unexpected changes |
400| **Robust** | Valid HTML, ARIA, name/role/value | Invalid ARIA, duplicate IDs, missing roles |
401
402---
403
404## Running Tests
405
406### Maven Commands
407
408| Command | Purpose |
409|---------|---------|
410| `mvn test -Dgroups=a11y` | Run all accessibility tests |
411| `mvn test -Dtest=A11yTest` | Run specific test class |
412| `mvn test -Dheadless=true` | Run headless (CI mode) |
413| `mvn allure:serve` | View Allure report with violations |
414
415### CI/CD Integration
416
417```yaml
418- name: Run Accessibility Tests
419 run: mvn test -Dgroups=a11y -Dheadless=true
420
421- name: Upload A11y Report
422 uses: actions/upload-artifact@v3
423 with:
424 name: a11y-report
425 path: target/a11y-results/
426```
427
428---
429
430## Common Rationalizations
431
432> Common shortcuts and "good enough" excuses that erode test quality — and the reality behind each.
433
434| Rationalization | Reality |
435| --------------- | ------- |
436| "Selenium isn't good for a11y testing" | axe-core + Selenium is battle-tested, CI-ready, and covers WCAG violations programmatically. |
437| "We can just run a scan at the end" | Shift-left: catch violations as code is written. Late scans mean expensive fixes. |
438| "The framework handles accessibility" | No framework auto-generates proper ARIA roles, labels, or keyboard interactions. |
439| "We only need to test the homepage" | Every page a user visits must be accessible. Start with high-risk pages, expand coverage. |
440| "Skip the contrast checks, designers fix that" | Automated contrast checks take seconds and prevent lawsuits. They are tests, not design reviews. |
441| "Our users don't have disabilities" | ~15% of the global population has some form of disability. Accessibility is for everyone. |
442
443---
444
445## References
446
447- [Axe Patterns Guide](references/axe_patterns.md) - AxeBuilder patterns and helpers
448- [WCAG 2.1 AA Checklist](references/wcag21aa-checklist.md) - Manual audit checklist
449- [Deque Axe Rules](https://dequeuniversity.com/rules/axe/4.10) - Rule descriptions
450- [W3C WCAG 2.1](https://www.w3.org/TR/WCAG21/) - Official specification
451- [WAI-ARIA Practices](https://www.w3.org/WAI/ARIA/apg/) - Widget patterns
452
453---
454
455## Quick Reference
456
457| Task | Code Pattern |
458|------|--------------|
459| Full page scan | `new AxeBuilder().withTags(List.of("wcag2aa")).analyze(driver)` |
460| Component scan | `new AxeBuilder().include("#selector").analyze(driver)` |
461| Exclude element | `new AxeBuilder().exclude(".ignore").analyze(driver)` |
462| Check violations | `results.getViolations().isEmpty()` |
463| Filter critical | `.filter(v -> v.getImpact().equals("critical"))` |
464| Get help URL | `violation.getHelpUrl()` |
465| Tab navigation | `element.sendKeys(Keys.TAB)` |
466| Get focused element | `driver.switchTo().activeElement()` |
467
468---
469
470## Verification
471
472After completing this skill's workflow, confirm:
473
474- [ ] **Axe WebDriver audit passes** — `AxeBuilder.analyze(driver)` returns zero violations
475- [ ] **WCAG 2.1 AA compliance** — All rules for AA level pass
476- [ ] **ARIA labels present** — All interactive elements have accessible names
477- [ ] **Keyboard accessibility verified** — Tab navigation reaches all interactive elements
478- [ ] **Violation report saved** — Accessibility results written to JSON/HTML file
479- [ ] **Tests pass with Java 21+** — `mvn test -Dtest=*Accessibility*` passes