Deque Attest: Enterprise Accessibility Testing at Scale
Deque Attest (formerly WorldSpace Attest) is Deque's enterprise accessibility platform. It sits above axe DevTools and axe-core in Deque's product stack, targeting organizations that need coordinated testing across dozens of apps, centralized reporting, and issue tracking integrated into existing workflows.
This post covers where Attest fits, how to automate it, and how it compares to the axe DevTools browser extension that most accessibility engineers already use.
The Deque Product Stack
Before diving into Attest, clarify the relationship between Deque's products:
- axe-core: Open-source library. Powers everything else. Integrates directly into test frameworks.
- axe DevTools: Browser extension + API. Manual and semi-automated testing. Per-developer licensing.
- Deque Attest: Enterprise platform. Centralized scanning, dashboards, organization-wide reporting, integrations with JIRA/GitHub/Azure DevOps.
If your team has 5 developers all running axe DevTools and emailing spreadsheets to a compliance lead, that's Attest's target problem. It consolidates results, tracks remediation progress, and surfaces trends across your entire application portfolio.
Scanning SPAs with Attest
Single-page applications require explicit attention because Attest's crawler can't follow JavaScript-rendered routes the way it follows <a> tags in server-rendered HTML. You have two options: sitemap-driven scanning or authenticated session scanning.
Sitemap-Driven Scanning
Provide Attest with a sitemap and it will scan each URL independently. For SPAs, you need to generate a sitemap that includes all routes — not just the landing page:
// generate-sitemap.js — outputs all SPA routes for Attest
const routes = [
'/',
'/dashboard',
'/dashboard/reports',
'/dashboard/settings',
'/users',
'/users/profile',
'/billing',
'/billing/invoices'
];
const baseUrl = process.env.STAGING_URL || 'https://staging.example.com';
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes.map(r => ` <url><loc>${baseUrl}${r}</loc></url>`).join('\n')}
</urlset>`;
require('fs').writeFileSync('sitemap-attest.xml', sitemap);In Attest's scan configuration, point to this sitemap URL and set a post-load delay (typically 1000–2000ms) to allow JavaScript rendering to complete before axe runs.
Authenticated Scanning
Many SPA routes are behind authentication. Attest supports cookie-based and header-based auth. Configure it via the scan API:
{
"scanConfig": {
"urls": ["https://staging.example.com/dashboard"],
"authType": "cookie",
"cookies": [
{
"name": "session",
"value": "{{SESSION_TOKEN}}",
"domain": "staging.example.com"
}
],
"waitFor": {
"selector": "[data-testid='dashboard-loaded']",
"timeout": 5000
}
}
}The waitFor option is critical for SPAs. Without it, Attest scans the loading skeleton rather than the rendered content.
Attest vs axe DevTools: When to Use Each
| Scenario | axe DevTools | Deque Attest |
|---|---|---|
| Developer fixing a specific component | Yes — fast feedback in browser | Overkill |
| QA engineer auditing a new feature | Yes — manual guided testing | Possible |
| Compliance team auditing 50 apps | No — manual, unscalable | Yes |
| Tracking remediation progress over quarters | No — no persistence | Yes |
| Integration with JIRA backlog | Manual copy-paste | Native |
| Automated CI scanning | Use axe-core directly | Via Attest API |
axe DevTools is a per-session tool. Attest is an organizational system. They complement each other — developers use DevTools for immediate feedback, Attest provides the audit trail and portfolio view.
JIRA Integration
Attest can push violations directly to JIRA as bugs. Configure the integration in Attest's admin panel with your JIRA project key, issue type, and field mappings. Each violation becomes a ticket with:
- WCAG criterion code and description
- Affected URL
- Element selector
- Recommended fix from Deque's guidance
- Screenshot of the violation in context
Critically, Attest deduplicates. The same violation on the same element across multiple scans creates one ticket, not dozens. It updates the ticket status when the violation is resolved in subsequent scans.
For GitHub Issues, the integration works similarly. Configure via Attest's integrations panel:
{
"integration": "github",
"repo": "your-org/your-repo",
"labels": ["accessibility", "wcag2a"],
"assignees": [],
"body_template": "**Violation**: {{rule_description}}\n**URL**: {{url}}\n**WCAG**: {{wcag_criteria}}\n**Selector**: `{{selector}}`\n\n**How to fix**: {{help_url}}"
}Using the Attest API for Custom Automation
Attest exposes a REST API for programmatic scan control. This is how you integrate it into CI beyond the built-in CI plugins:
// attest-scan.js
const ATTEST_API = 'https://attest.deque.com/api/v1';
const headers = {
'Authorization': `Bearer ${process.env.ATTEST_API_KEY}`,
'Content-Type': 'application/json'
};
async function startScan(urls, projectId) {
const response = await fetch(`${ATTEST_API}/scans`, {
method: 'POST',
headers,
body: JSON.stringify({
projectId,
urls,
standard: 'WCAG2AA',
runners: ['axe'],
waitDelay: 1500
})
});
return response.json();
}
async function pollScanStatus(scanId) {
while (true) {
const response = await fetch(`${ATTEST_API}/scans/${scanId}`, { headers });
const scan = await response.json();
if (scan.status === 'complete') return scan;
if (scan.status === 'failed') throw new Error(`Scan failed: ${scan.error}`);
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
async function getScanResults(scanId) {
const response = await fetch(`${ATTEST_API}/scans/${scanId}/results`, { headers });
return response.json();
}
async function main() {
const urls = process.argv.slice(2);
const projectId = process.env.ATTEST_PROJECT_ID;
console.log(`Starting scan of ${urls.length} URLs...`);
const { id: scanId } = await startScan(urls, projectId);
console.log(`Scan ${scanId} started. Polling...`);
await pollScanStatus(scanId);
const results = await getScanResults(scanId);
const critical = results.violations.filter(v => v.impact === 'critical' || v.impact === 'serious');
console.log(`Total violations: ${results.violations.length}`);
console.log(`Critical/Serious: ${critical.length}`);
if (critical.length > 0) {
console.error('Critical violations found:');
critical.forEach(v => console.error(` - [${v.impact}] ${v.id}: ${v.description}`));
process.exit(1);
}
}
main().catch(console.error);Organization-Wide Reporting
Attest's dashboard shows violation trends across all your projects. The most useful metrics for engineering leaders:
- Violation trend: Are total violations going up or down across the portfolio?
- Mean time to remediate: How long from detection to fix for each severity level?
- Coverage: What percentage of routes have been scanned in the last 30 days?
- New violations per sprint: Are teams introducing new issues as fast as they fix them?
Export these reports via the API for inclusion in quarterly accessibility audits or board-level compliance reports:
async function generatePortfolioReport(dateRange) {
const response = await fetch(
`${ATTEST_API}/reports/portfolio?from=${dateRange.from}&to=${dateRange.to}&format=json`,
{ headers }
);
return response.json();
}Practical Limitations
Attest is not a replacement for manual accessibility testing or screen reader testing. Its automation catches the same ~30–40% of WCAG issues that any axe-based scanner finds. The value Attest adds over running axe-core directly is:
- Persistence and history — scans are stored, violations are tracked over time
- Scale — scan hundreds of URLs without custom tooling
- Workflow integration — JIRA/GitHub tickets created automatically
- Team coordination — multiple teams see a shared view of the accessibility backlog
For teams already using axe-core in CI, the argument for Attest is organizational, not technical. If you have one accessibility engineer and five developers, axe-core in CI is sufficient. If you have a compliance officer trying to audit 40 apps across 8 teams, Attest's centralization is worth the licensing cost.
Combine Attest for portfolio-level scanning with axe-core in unit tests for component-level coverage, and behavioral Playwright tests for interaction coverage. Each layer catches what the others miss.