Use the accessFlow SDK with Playwright or Selenium for Java applications to run automated accessibility audits as part of your test suite.
Supports JUnit 5, JUnit 4, TestNG, and Cucumber.
How it works
Local development
- Audits run on the selected pages
- JSON reports saved to
./test-resultsdirectory - Reports are NOT uploaded to the accessFlow Audits page (prevents accidental uploads)
CI/CD environment
- Audits run on the selected pages
- Results aggregated from parallel test workers
- Automatically uploaded to the accessFlow Audits page
- Threshold checks applied (fail build if exceeded)
Prerequisites
You'll need
- Registry install token: Used to install the SDK package (for remote installation - provided by accessiBe)
- Maven registry URL: The private package registry location (for remote installation - provided by accessiBe)
- SDK API key: Used to authenticate audit requests at runtime (see Generate a token)
Important: The Registry install token is used to download/install the SDK package. The SDK API key is used at runtime when running accessibility audits. These are two separate credentials.
Requirements
- Java 11+
- Playwright or Selenium for Java
- Maven or Gradle
- Gson
Install the SDK
Install from a private package registry (recommended)
Maven (pom.xml)
Add the accessFlow repository and dependency:
<repositories>
<repository>
<id>accessflow-registry</id>
<url>https://ACCESSFLOW_MAVEN_REGISTRY_URL</url>
</repository>
</repositories>
<dependency>
<groupId>com.acsbe</groupId>
<artifactId>accessflow-sdk</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
Add a runtime dependency:
- Playwright:
com.microsoft.playwright:playwright - Selenium:
org.seleniumhq.selenium:selenium-java
Configure credentials in ~/.m2/settings.xml:
<settings>
<servers>
<server>
<id>accessflow-registry</id>
<username>_json_key_base64</username>
<password>REGISTRY_INSTALL_TOKEN</password>
</server>
</servers>
</settings>Gradle (build.gradle)
repositories {
maven {
url "https://ACCESSFLOW_MAVEN_REGISTRY_URL"
credentials {
username = "_json_key_base64"
password = project.findProperty("REGISTRY_INSTALL_TOKEN")
?: System.getenv("ACCESSFLOW_REGISTRY_TOKEN")
}
}
}
dependencies {
testImplementation 'com.acsbe:accessflow-sdk:1.1.0'
}Install from a local JAR
If you received a JAR file:
Install to the local Maven repository
mvn install:install-file -Dfile=/path/to/accessflow-sdk.jar -DgroupId=com.acsbe -DartifactId=accessflow-sdk -Dversion=1.1.0 -Dpackaging=jar
Then use the dependency block normally.
Configure the SDK API key
First generate a token in accessFlow and then use it to set the API key.
Generate a token
- Go to your Profile menu in top right corner and select Token management.
- Select Generate token.
- Enter a token name.
- Select Audit SDK token and then Generate token.
- Copy the token and store it securely. You won't be able to see it again.
Provide the API key
Recommended: Use an environment variable
Provide your accessFlow API key using an environment variable (recommended):
export ACCESSFLOW_SDK_API_KEY=your-api-key-hereThen initialize normally:
AccessFlowSDK sdk = new AccessFlowSDK(page);
Playwright example
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page);
page.navigate("<https://example.com>");
Map<String, Object> audits = sdk.audit();
Map<String, Object> report = sdk.generateReport(audits);Selenium example
WebDriver driver = new ChromeDriver();
AccessFlowSDK sdk =
new AccessFlowSDK(new SeleniumDriver(driver));
driver.get("<https://example.com>");
Map<String, Object> audits = sdk.audit();
Map<String, Object> report = sdk.generateReport(audits);
driver.quit();Integrate with the test suite
Note: Manual teardown is not needed in modern frameworks.
Automatic behavior
-
sdk.audit()automatically records results - Reports are automatically aggregated
- Reports are automatically uploaded in CI environments
| Framework | Behavior |
|---|---|
| JUnit 5 | Fully automatic |
| TestNG 7.6+ | Fully automatic |
| TestNG <7.6 | Manual listener required |
| JUnit 4 | Manual finalize required |
JUint 5 example (recommended)
This is an example of how to use the SDK.
@Test
public void testAccessibility() {
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page);
page.navigate("https://example.com");
sdk.audit(); // auto-recorded
page.close();
}JUnit 4 example (manual)
In this example, AccessFlowTeardown.finalizeReports() is needed.
@AfterClass
public static void teardown() {
AccessFlowTeardown.finalizeReports();
}Configure the SDK (optional
Create an accessflow.config.json file in your project root. Use it to define thresholds for accessibility issues that will fail your test suite if exceeded.
If no config file is provided, thresholds are not enforced (tests won’t fail because of counts). Reports are still generated.
Example:
{
"issuesFoundThreshold": {
"extreme": 0,
"high": 5,
"medium": 10,
"low": 20
},
"localCheck": false
}Options
| Parameter | Type | Description |
|---|---|---|
| issuesFoundThreshold | object |
(Optional) Defines the maximum allowed number of accessibility issues per severity. Non-numeric values are ignored.
|
| localCheck | boolean |
(Optional) Whether threshold checking is enforced during test execution. Default is false.
|
Final report and localCheck
Final report
After all tests are complete, the SDK's listener (JUnit 5 / TestNG) or your manual AccessFlowTeardown.finalizeReports() call aggregates all recorded audits into one report, uploads it to the accessFlow Audits page in CI environments, and validates it against issuesFoundThreshold.
The report JSON includes pages (keyed by URL: numberOfIssuesFound, ruleViolations with description, severity, selectors, selectorData with HTML/suggestionLabel/suggestionType, WCAG level/link) and totalNumberOfIssuesFound.
Example shape (abbreviated):
{
"pages": {
"https://example.com/1": {
"numberOfIssuesFound": { "extreme": 0, "high": 1, "medium": 1, "low": 0 },
"ruleViolations": {
"IMG_MISSING_ALT": {
"description": "Images must have alternate text",
"name": "Images must have alternate text",
"severity": "high",
"selectors": ["img.hero"],
"selectorData": [{ "selector": "img.hero", "HTML": "<img src=\\"/banner.png\\">", "suggestionLabel": "Add alt text", "suggestionType": "addAttribute" }],
"WCAGLevel": "A",
"WCAGLink": "https://www.w3.org/TR/WCAG21/#text-alternatives"
},
"HEADING_ORDER": {
"description": "Headings must be in order",
"name": "Heading order",
"severity": "medium",
"selectors": ["h3:first-of-type"],
"selectorData": [{ "selector": "h3:first-of-type", "HTML": "<h3>Section</h3>", "suggestionLabel": "Use h2 before h3", "suggestionType": "changeTag" }],
"WCAGLevel": "A",
"WCAGLink": "https://www.w3.org/TR/WCAG21/#info-and-relationships"
}
}
},
"https://example.com/2": {
"numberOfIssuesFound": { "extreme": 0, "high": 1, "medium": 1, "low": 0 },
"ruleViolations": {
"LINK_EMPTY": {
"description": "Links must have discernible text",
"name": "Links must have discernible text",
"severity": "high",
"selectors": ["a.nav-link", "a[href='#']"],
"selectorData": [
{ "selector": "a.nav-link", "HTML": "<a class=\\"nav-link\\" href=\\"/about\\"></a>", "suggestionLabel": "Add link text", "suggestionType": "addContent" },
{ "selector": "a[href='#']", "HTML": "<a href=\\"#\\"> </a>", "suggestionLabel": "Add link text", "suggestionType": "addContent" }
],
"WCAGLevel": "A",
"WCAGLink": "https://www.w3.org/TR/WCAG21/#link-purpose-in-context"
},
"CONTRAST_MINIMUM": {
"description": "Text must meet minimum contrast ratio",
"name": "Contrast (Minimum)",
"severity": "medium",
"selectors": [".muted"],
"selectorData": [{ "selector": ".muted", "HTML": "<span class=\\"muted\\">Disclaimer</span>", "suggestionLabel": "Increase contrast", "suggestionType": "changeStyle" }],
"WCAGLevel": "AA",
"WCAGLink": "https://www.w3.org/TR/WCAG21/#contrast-minimum"
}
}
}
},
"totalNumberOfIssuesFound": { "extreme": 0, "high": 2, "medium": 2, "low": 0 }
}localCheck
To fail local runs when thresholds are exceeded, set "localCheck": true in accessflow.config.json:
{
"issuesFoundThreshold": { "extreme": 0, "high": 5, "medium": 10, "low": 20 },
"localCheck": true
}With localCheck: true, running mvn test locally will exit with an error and print issue counts if the aggregated report exceeds the configured limits.
API Reference
AccessFlowSDK
Constructors
AccessFlowSDK(Page page) AccessFlowSDK(Page page, String apiKey)
Parameters
| Parameter | Description |
|---|---|
| page | Playwright Page object |
| apiKey | The API key generated |
Run an audit
Runs an accessibility audit on the current page.
sdk.audit()
Map<String, Object> audits = sdk.audit();
Returns
Raw audit data as a dictionary.
Generate a report
Generates a structured summary report from audit data.
sdk.generate_report(audits)
Map<String, Object> report = sdk.generateReport(audits)
Returns
- Issue counts by severity
- Rule violations
- WCAG level
- Descriptions
- CSS selectors
AccessFlowTeardown
Manual teardown is usually handled automatically.
AccessFlowTeardown.finalizeReports();
Integrate with your CI/CD
In your CI pipelines, configure these secrets and variables. This will ensure your test results are displayed in the accessFlow Audits page.
Secrets
ACCESSFLOW_REGISTRY_TOKEN: To install the SDKACCESSFLOW_SDK_API_KEY: To authenticate audit requests
Variables
| Variable | Purpose | Example |
|---|---|---|
GCP_SDK_MAVEN_REGISTRY_URL |
Maven registry URL (full URL with https://) |
https://us-east1-maven.pkg.dev/PROJECT/stag-accessflow-maven |
Note: Maven requires the full URL including https:// unlike npm and pip which add it automatically.
Maven example
Generate settings.xml dynamically in your pipeline using the secret:
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<servers>
<server>
<id>accessflow-registry</id>
<username>_json_key_base64</username>
<password>${ACCESSFLOW_REGISTRY_TOKEN}</password>
</server>
</servers>
</settings>
EOF
GitHub Actions example
name: Accessibility Tests
on: [push]
jobs:
test:
runs-on: ubuntu-22.04 # Use 22.04 for Playwright compatibility
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Configure Maven authentication
env:
ACCESSFLOW_REGISTRY_TOKEN: ${{ secrets.ACCESSFLOW_REGISTRY_TOKEN }}
GCP_SDK_MAVEN_REGISTRY_URL: ${{ vars.GCP_SDK_MAVEN_REGISTRY_URL }}
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<servers>
<server>
<id>accessflow-registry</id>
<username>_json_key_base64</username>
<password>${ACCESSFLOW_REGISTRY_TOKEN}</password>
</server>
</servers>
<profiles>
<profile>
<id>accessflow</id>
<repositories>
<repository>
<id>accessflow-registry</id>
<url>${GCP_SDK_MAVEN_REGISTRY_URL}</url>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>accessflow</activeProfile>
</activeProfiles>
</settings>
EOF - name: Resolve dependencies
run: mvn dependency:resolve
- name: Install Playwright browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.classpathScope=test -D exec.args="install --with-deps chromium"
- run: mvn test
env:
ACCESSFLOW_SDK_API_KEY: ${{ secrets.ACCESSFLOW_SDK_API_KEY }}Jenkins example
pipeline {
agent any
environment {
ACCESSFLOW_REGISTRY_TOKEN = credentials('accessflow-registry-token')
ACCESSFLOW_SDK_API_KEY = credentials('accessflow-api-key')
}
stages {
stage('Setup') {
steps {
sh '''
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<servers>
<server>
<id>accessflow-registry</id>
<username>_json_key_base64</username>
<password>${ACCESSFLOW_REGISTRY_TOKEN}</password>
</server>
</servers>
</settings>
EOF
'''
}
}
stage('Install Playwright') {
steps {
sh 'mvn dependency:resolve'
sh 'mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.classpathScope=test -D exec.args="install --with-deps chromium"'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
}
}Gradle example
Pass the token as an environment variable:
ACCESSFLOW_REGISTRY_TOKEN=your-base64-token mvn test
The build.gradle configuration reads from this environment variable automatically.
Framework integration
JUnit 5 (recommended - zero config)
The SDK ships a TestExecutionListener registered via Java ServiceLoader. JUnit 5 picks it up automatically - no annotations or configuration needed.
Reports are finalized and uploaded once after the whole test plan finishes.
import com.acsbe.accessflow.*;
import org.junit.jupiter.api.*;
import java.util.Map;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class AccessibilityTest {
private Playwright playwright;
private Browser browser;
@BeforeAll void setup() {
playwright = Playwright.create();
browser = playwright.chromium().launch();
}
@AfterAll void teardown() {
// AccessFlowTeardown.finalizeReports() is called automatically by
// the SDK's TestExecutionListener — no manual call needed.
if (browser != null) browser.close();
if (playwright != null) playwright.close();
}
@Test
public void testAccessibility() {
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page);
page.navigate("https://example.com");
sdk.audit(); // automatically records results
page.close();
}
}JUnit 4 (manual teardown required)
JUnit 4 has no TestExecutionListener concept, so you must call finalizeReports() yourself. The recommended approach is a @ClassRule or @AfterClass:
import com.acsbe.accessflow.*;
import org.junit.*;
import java.util.Map;
public class AccessibilityTest {
private static Playwright playwright;
private static Browser browser;
@BeforeClass
public static void setup() {
playwright = Playwright.create();
browser = playwright.chromium().launch();
}
@AfterClass
public static void teardown() {
// Finalize and upload all recorded audits at the end of the class
AccessFlowTeardown.finalizeReports();
if (browser != null) browser.close();
if (playwright != null) playwright.close();
}
@Test
public void testAccessibility() {
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page);
page.navigate("https://example.com");
sdk.audit(); // automatically records results
page.close();
}
}If you have multiple test classes and want a single suite-level teardown, register a JUnit 4 RunListener on your test runner (e.g. Maven Surefire):
```java
// src/test/java/com/example/AccessFlowRunListener.java
import com.acsbe.accessflow.AccessFlowTeardown;
import org.junit.runner.notification.RunListener;
import org.junit.runner.Description;
public class AccessFlowRunListener extends RunListener {
@Override
public void testRunFinished(org.junit.runner.Result result) {
AccessFlowTeardown.finalizeReports();
}
}
```
```xml
<!-- pom.xml — register listener with Surefire -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<properties>
<property>
<name>listener</name>
<value>com.example.AccessFlowRunListener</value>
</property>
</properties>
</configuration>
</plugin>
```TestNG (automatic via ServiceLoader - zero config with TestNG 7.6+)
The SDK ships an ISuiteListener (AccessFlowSuiteListener) registered via Java ServiceLoader. TestNG 7.6+ discovers it automatically from the JAR - no annotations or testng.xml changes needed.
Reports are finalized and uploaded once after the whole suite finishes.
Zero-config (TestNG 7.6+)
Just run your tests normally - the listener is auto-registered:
import com.acsbe.accessflow.*;
import org.testng.annotations.*;
import java.util.Map;
public class AccessibilityTest {
private Playwright playwright;
private Browser browser;
@BeforeClass
public void setup() {
playwright = Playwright.create();
browser = playwright.chromium().launch();
}
@AfterClass
public void teardown() {
// AccessFlowSuiteListener finalizes reports automatically after the suite.
// No manual call needed here.
if (browser != null) browser.close();
if (playwright != null) playwright.close();
}
@Test
public void testAccessibility() {
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page);
page.navigate("https://example.com");
sdk.audit(); // automatically records results
page.close();
}
}Explicit registration (older TestNG or if ServiceLoader is disabled)
Option A - testng.xml
<suite name="Accessibility Suite">
<listeners>
<listener class-name="com.acsbe.accessflow.AccessFlowSuiteListener"/>
</listeners>
<test name="Accessibility Tests">
<classes>
<class name="com.example.AccessibilityTest"/>
</classes>
</test>
</suite>Option B - @Listeners annotation on your test class
import com.acsbe.accessflow.AccessFlowSuiteListener;
import org.testng.annotations.Listeners;
@Listeners(AccessFlowSuiteListener.class)
public class AccessibilityTest {
// ...
}Cucumber
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.en.*;
public class AccessibilitySteps {
private Page page;
private AccessFlowSDK sdk;
@Before
public void setup() {
Playwright playwright = Playwright.create();
Browser browser = playwright.chromium().launch();
page = browser.newPage();
sdk = new AccessFlowSDK(page);
}
@When("I visit the homepage")
public void visitHomepage() {
page.navigate("https://example.com");
}
@Then("the page should be accessible")
public void checkAccessibility() {
Map<String, Object> audits = sdk.audit();
// Note: audit() automatically records results
Map<String, Object> report = sdk.generateReport(audits);
@SuppressWarnings("unchecked")
Map<String, Object> issues =
(Map<String, Object>) report.get("numberOfIssuesFound");
assertEquals(0, issues.getOrDefault("extreme", 0));
}
@After
public void teardown() {
if (page != null) page.close();
}
}Advanced usage
Running an audit on multiple pages
```java
@Test
public void testMultiplePages() {
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page);
// Test homepage
page.navigate("https://example.com");
Map<String, Object> homeAudits = sdk.audit();
// Note: audit() automatically records results
// Test about page
page.navigate("https://example.com/about");
Map<String, Object> aboutAudits = sdk.audit();
// Note: audit() automatically records results
page.close();
}
```Setting a custom API key per test
@Test
public void testWithCustomKey() {
Page page = browser.newPage();
AccessFlowSDK sdk = new AccessFlowSDK(page, "custom-api-key");
page.navigate("https://example.com");
Map<String, Object> audits = sdk.audit();
Map<String, Object> report = sdk.generateReport(audits);
// Assertions...
page.close();
}Troubleshooting
API key issues
// Check if environment variable is set
System.out.println(System.getenv("ACCESSFLOW_SDK_API_KEY"));
// Pass API key explicitly (recommended)
AccessFlowSDK sdk = new AccessFlowSDK(page, "your-api-key");
// Or set as system property (for testing)
System.setProperty("ACCESSFLOW_SDK_API_KEY", "your-api-key");Never hardcode API keys. Use:
- Constructor parameters for local development
- Environment variables for CI/CD (stored as secrets)
- System properties for test runs
- Configuration files excluded from version control
Reports not uploading
- Verify API key has upload permissions
- Check AccessFlowTeardown.finalizeReports() is called after all tests
Playwright browser not found
# Install Playwright browsers
# Note: -D exec.classpathScope=test is required because Playwright is a test-scoped dependency
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.classpathScope=test -D exec.args="install --with-deps chromium"Build errors
# Clean and rebuild mvn clean install # Update dependencies mvn dependency:resolve