skills/accessibility-selenium-testing/SKILL.md
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).
npx skillsauth add jyjeanne/ai-setup-forge accessibility-selenium-testingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
| 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:seleniumMaven dependency for axe integration.
| 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 |
| 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 |
| 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 |
| 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 |
new AxeBuilder().analyze(driver)new AxeBuilder().include("#my-component").analyze(driver).withTags(List.of("wcag2a", "wcag2aa")).exclude(".legacy-footer") (use carefully, document reason)Results.getViolations() - should be emptyResults to JSON for dashboardsAs an Accessibility Automation Specialist:
AxeBuilder with appropriate WCAG tagsAdd 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
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
element.sendKeys(Keys.TAB);
WebElement focused = driver.switchTo().activeElement();
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
@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();
}
@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();
}
@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();
}
/**
* 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();
}
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));
}
}
}
@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();
}
}
| 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 |
✅ 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
⚠️ 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
| 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 |
| 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 |
- 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/
| 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() |
development
Generate breadboard circuit mockups and visual diagrams using HTML5 Canvas drawing techniques. Use when asked to create circuit layouts, visualize electronic component placements, draw breadboard diagrams, mockup 6502 builds, generate retro computer schematics, or design vintage electronics projects. Supports 555 timers, W65C02S microprocessors, 28C256 EEPROMs, W65C22 VIA chips, 7400-series logic gates, LEDs, resistors, capacitors, switches, buttons, crystals, and wires.
development
Apply lean thinking to UX: hypothesis-driven design, collaborative sketching, and rapid experiments instead of heavy deliverables. Use when the user mentions "Lean UX", "design hypothesis", "UX experiment", "collaborative design", or "outcome over output". Covers hypothesis statements, MVPs for UX, and cross-functional collaboration. For Build-Measure-Learn, see lean-startup. For usability audits, see ux-heuristics.
development
Design MVPs, validated learning experiments, and pivot-or-persevere decisions using Build-Measure-Learn. Use when the user mentions "MVP scope", "validated learning", "pivot or persevere", "vanity metrics", or "test assumptions". Covers innovation accounting and actionable metrics. For 5-day prototype testing, see design-sprint. For customer motivation analysis, see jobs-to-be-done.
tools
Instrument, trace, evaluate, and monitor LLM applications and AI agents with LangSmith. Use when setting up observability for LLM pipelines, running offline or online evaluations, managing prompts in the Prompt Hub, creating datasets for regression testing, or deploying agent servers. Triggers on: langsmith, langchain tracing, llm tracing, llm observability, llm evaluation, trace llm calls, @traceable, wrap_openai, langsmith evaluate, langsmith dataset, langsmith feedback, langsmith prompt hub, langsmith project, llm monitoring, llm debugging, llm quality, openevals, langsmith cli, langsmith experiment, annotate llm, llm judge.