Audit accessibility with axe-core
The getAxeAccessibilityViolations extension runs axe-core accessibility checks against the current browser page and returns a compact JSON summary of detected violations, incomplete checks, impact counts, and selected violation details.
This is useful when you want a Virtuoso journey to audit a full page, scope the audit to a specific page region, skip noisy third-party widgets, filter by impact level, or save a JSON audit artefact that can be processed later for reporting and defect-management workflows.
Parameters:
-
tagsoptional, a string containing comma-separated axe-core rule tags. When omitted or blank, the extension useswcag2a,wcag2aa,wcag21a,wcag21aa,wcag22aa,best-practice; -
includeoptional, a CSS selector string used to scope the audit to a specific element or region, for example#main. When omitted or blank, the full document is audited unlessexcludeis supplied; -
excludeoptional, a CSS selector string used to skip specific elements, for example#cookie-banneror#chat-widget, #cookie-banner; -
minImpactoptional, a string filter floor for returned violations. Accepted values areminor,moderate,serious, andcritical. Invalid, unsupported, or blank values do not filter the returned violations; -
summaryOnlyoptional, a boolean or string value. Usetrueor"true"to omit per-node DOM details from the response and reduce the returned payload size; -
downloadoptional, a boolean or string value. Usefalseor"false"to skip the JSON file download and only return the payload. When omitted, blank,true, or"true", the extension also attempts to download the report as anaxe-report-<timestamp>.jsonfile.
Note: Inputs are optional and positional. To provide a later input while skipping an earlier one, pass an empty string for the skipped input, for example "" as include before passing exclude, minImpact, summaryOnly, or download. CSS selectors must match elements in the rendered page state available when the step runs.
How to apply this to your journey
Use the extension in a journey by calling getAxeAccessibilityViolations with the execute command. Pass each value to the matching extension input using as inputName.
Note: The extension is asynchronous and returns a JSON string through done(json). Store the result in a variable, then assert against fields such as $a11y.violationCount, $a11y.impactCounts.critical, or $a11y.impactCounts.serious. The download input only controls the browser-side JSON file download; the JSON payload is still returned to the journey on success.
New version update - 18/06/2026: Added support for downloading the generated axe JSON report as a browser-side side-effect artefact. Customers can keep the returned payload for assertions and also use the downloaded JSON for downstream processing, such as generating Axe HTML reports, attaching evidence to accessibility reviews, or raising tickets in ticket-management systems so developers can triage and fix the issues.
// Full page audit using the default WCAG and best-practice tags
execute "getAxeAccessibilityViolations" returning $a11y
Assert ${ $a11y.violationCount } equals "0"
// Only critical and serious violations, scoped to #app and excluding a noisy widget
execute "getAxeAccessibilityViolations" using "wcag2aa,wcag21aa" as tags, "#app" as include, ".intercom-launcher" as exclude, "serious" as minImpact returning $a11y
Assert ${ $a11y.impactCounts.critical } equals "0"
Assert ${ $a11y.impactCounts.serious } equals "0"// Summary-only response without per-node DOM detail
execute "getAxeAccessibilityViolations" using "wcag2aa" as tags, "" as include, "" as exclude, "" as minImpact, "true" as summaryOnly returning $a11y
Assert ${ $a11y.violationCount } equals "0"
// Return the payload but skip the JSON file download
execute "getAxeAccessibilityViolations" using "" as tags, "" as include, "" as exclude, "" as minImpact, "" as summaryOnly, "false" as download returning $a11y
Assert ${ $a11y.violationCount } equals "0"You can also pass the tag list, scope, exclusion selector, severity floor, summary-only flag, and download flag from variables.
execute "getAxeAccessibilityViolations" using "$auditTags" as tags, "$auditScope" as include, "$excludeSelector" as exclude, "$minimumImpact" as minImpact, "$summaryOnly" as summaryOnly, "$downloadReport" as download returning $a11ystore value "wcag2aa,wcag21aa" in $auditTags
store value "#main" in $auditScope
store value "#cookie-banner" in $excludeSelector
store value "serious" in $minimumImpact
store value "true" in $summaryOnly
store value "true" in $downloadReport
execute "getAxeAccessibilityViolations" using "$auditTags" as tags, "$auditScope" as include, "$excludeSelector" as exclude, "$minimumImpact" as minImpact, "$summaryOnly" as summaryOnly, "$downloadReport" as download returning $a11y
Assert ${ $a11y.violationCount } equals "0"Example output from a summary-only run when violations are found:
{
"url": "https://example.com/",
"timestamp": "2026-06-18T10:00:00.000Z",
"testEngine": {
"name": "axe-core",
"version": "4.11.0"
},
"violationCount": 1,
"incompleteCount": 0,
"impactCounts": {
"critical": 0,
"serious": 1,
"moderate": 0,
"minor": 0
},
"violations": [
{
"id": "label",
"impact": "serious",
"help": "Form elements must have labels",
"helpUrl": "https://dequeuniversity.com/rules/axe/4.11/label",
"description": "Ensures every form element has a label",
"tags": [
"cat.forms",
"wcag2a",
"wcag412"
],
"nodeCount": 1
}
]
}This extension requires the following resources:
The extension should be configured as:
- Run asynchronously: Yes
- Scope: Global
Limitation: This extension depends on axe-core 4.11.0, the configured external jsDelivr resource, and the rendered browser DOM available when the Virtuoso journey step runs. It audits the DOM state available to axe.run() at that moment, so it can miss content loaded after the audit starts, delayed SPA updates, virtualized or off-screen content, hidden menus and modals that have not been opened, content inside cross-origin or sandboxed iframes blocked by the browser same-origin policy, same-origin frames where axe-core cannot run, and content inside closed shadow DOM or browser-native controls. Third-party widgets, ads, maps, payment frames, SSO pages, CAPTCHA, and authentication-controlled frames may be skipped or reported incompletely. The extension runs only the selected comma-separated axe-core tags, and minImpact can intentionally hide lower-impact violations from the returned count. The result is compacted for journey assertions: node HTML is truncated to 500 characters, detailed axe check data, passes, and inapplicable rules are not returned, and incomplete checks are counted but not expanded. This extension is asynchronous and must be configured with Runs asynchronously; it calls done() on success and doneError() on failure. It has a source-code timeout of 90 seconds, so 90 seconds is the effective limit before the extension fails, even though async extension scripts must complete within Virtuoso's documented 120-second maximum execution window. The JSON download uses browser APIs such as Blob, URL.createObjectURL(), an anchor click, and browser download handling. The returned payload is still the reliable value for journey assertions, while the downloaded file is a browser-side side-effect artefact and may be blocked, suppressed, unavailable in some remote or real-device executions, or affected by browser download policies. Network restrictions, content security policy, proxy rules, or blocked CDN access can prevent axe-core from loading. Cross-browser note: This extension runs in the browser or device selected for the journey execution. Because it uses document, CSS selectors, axe.run(), DOM traversal, iframe exposure, shadow-DOM-related browser behavior, Blob, URL.createObjectURL(), and browser file-download behavior, results and download availability can differ from Virtuoso's default Chromium-based browser in Safari, Firefox, Edge, iOS, Android, or remote-grid executions. Validate it in each browser/device configuration used by your plan. A zero-violation result means only that no matching automatable violations were returned for the audited DOM state; it must not be treated as full WCAG, RGAA, Section 508, EN 301 549, or overall accessibility compliance.
Add the extension to your Virtuoso instance
Select the domain that matches your Virtuoso account.
View source
Last updated: 18/06/2026
Resources:
/**
*
* // Simplest — full page, all WCAG violations
* getAxeAccessibilityViolations returning $a11y
* assert ${ $a11y.violationCount } equals "0"
*
* // Only critical/serious, skip a noisy widget
* getAxeAccessibilityViolations("wcag2aa,wcag21aa", "#app", ".intercom-launcher", "serious") returning $a11y
* assert ${ $a11y.impactCounts.critical } equals "0"
* assert ${ $a11y.impactCounts.serious } equals "0"
*
* // Fast check — no per-node DOM detail in payload
* getAxeAccessibilityViolations("wcag2aa", "", "", "", "true") returning $a11y
* assert ${ $a11y.violationCount } equals "0"
*
* // Skip the file download (only return the payload)
* getAxeAccessibilityViolations("", "", "", "", "", "false") returning $a11y
*/
// Optional positional params:
// tags comma-separated axe tags. Default: "wcag2a,wcag2aa,wcag21a,wcag21aa,wcag22aa"
// include CSS selector to scope the audit, e.g. "#main"
// exclude CSS selector to skip, e.g. ".third-party-widget"
// minImpact "minor" | "moderate" | "serious" | "critical" — filter floor
// summaryOnly "true" to omit per-node DOM detail (smaller payload)
// download "true" (default) to also save the report as a .json file;
// "false" to skip the file and only return the payload
//
// Returns: { url, timestamp, testEngine, violationCount, incompleteCount,
// impactCounts:{critical,serious,moderate,minor}, violations:[...] }
const DEFAULT_TAGS = ["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa","best-practice"];
const IMPACT_RANK = { minor:1, moderate:2, serious:3, critical:4 };
const RUN_TIMEOUT_MS = 90000; // Virtuoso's 120s extension cap, script timeout limit
const optTags = (typeof tags !== "undefined") ? tags : null;
const optInclude = (typeof include !== "undefined" && include !== "") ? include : null;
const optExclude = (typeof exclude !== "undefined" && exclude !== "") ? exclude : null;
const optMinImpact = (typeof minImpact !== "undefined" && minImpact)
? String(minImpact).toLowerCase() : null;
const optSummaryOnly = (typeof summaryOnly !== "undefined")
&& (summaryOnly === true || summaryOnly === "true");
const optDownload = (typeof download !== "undefined")
? !(download === false || download === "false")
: true;
if (typeof axe === "undefined" || typeof axe.run !== "function") {
doneError("axe-core not loaded. Check the Resources URL on this extension.");
} else {
const parseTags = t =>
!t ? DEFAULT_TAGS
: Array.isArray(t) ? t.filter(Boolean)
: String(t).split(",").map(s => s.trim()).filter(Boolean);
const buildContext = () => {
if (!optInclude && !optExclude) return document;
const ctx = {};
if (optInclude) ctx.include = [optInclude];
if (optExclude) ctx.exclude = [optExclude];
return ctx;
};
const filterByImpact = vs => {
if (!optMinImpact || !(optMinImpact in IMPACT_RANK)) return vs;
const floor = IMPACT_RANK[optMinImpact];
return vs.filter(v => IMPACT_RANK[v.impact || "minor"] >= floor);
};
const compactNodes = nodes => (nodes || []).map(n => ({
target: n.target,
html: (n.html || "").slice(0, 500),
failureSummary: n.failureSummary
}));
const buildResult = results => {
const filtered = filterByImpact(results.violations || []);
const impactCounts = { critical:0, serious:0, moderate:0, minor:0 };
filtered.forEach(v => {
const k = v.impact || "minor";
if (impactCounts[k] !== undefined) impactCounts[k]++;
});
return {
url: results.url,
timestamp: results.timestamp,
testEngine: results.testEngine,
violationCount: filtered.length,
incompleteCount: (results.incomplete || []).length,
impactCounts,
violations: filtered.map(v => {
const o = {
id: v.id,
impact: v.impact,
help: v.help,
helpUrl: v.helpUrl,
description: v.description,
tags: v.tags,
nodeCount: (v.nodes || []).length
};
if (!optSummaryOnly) o.nodes = compactNodes(v.nodes);
return o;
})
};
};
const saveReport = (report, json) => {
if (!optDownload) return;
try {
const ts = (report.timestamp || new Date().toISOString())
.replace(/[:.]/g, "-");
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `axe-report-${ts}.json`;
a.style.display = "none";
document.body.appendChild(a);
a.click();
// Revoke after the click has been processed.
setTimeout(() => {
URL.revokeObjectURL(url);
if (a.parentNode) a.parentNode.removeChild(a);
}, 1000);
} catch (e) {
}
};
const runOptions = {
resultTypes: ["violations", "incomplete"],
runOnly: { type: "tag", values: parseTags(optTags) }
};
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error(`axe.run timed out after ${RUN_TIMEOUT_MS}ms`)),
RUN_TIMEOUT_MS
);
});
Promise.race([axe.run(buildContext(), runOptions), timeoutPromise])
.then(results => {
clearTimeout(timeoutId);
const report = buildResult(results);
const json = JSON.stringify(report);
saveReport(report, json);
done(json);
})
.catch(err => {
clearTimeout(timeoutId);
doneError("getAxeAccessibilityViolations failed: " +
(err && err.message ? err.message : String(err)));
});
}
Comments
0 comments
Please sign in to leave a comment.