axe-core and shot-scraper for accessibility audits
I just watched a talk by Pamela Fox at North Bay Python on Automated accessibility audits. The video should be up within 24 hours.
One of the tools Pamela introduced us to was axe-core, which is a JavaScript library at the heart of a whole ecosystem of accessibility auditing tools.
I figured out how to use it to run an accessibility audit using my shot-scraper CLI tool:
shot-scraper javascript https://datasette.io "async () => { const axeCore = await import('https://cdn.jsdelivr.net/npm/axe-core@4.7.2/+esm'); return axeCore.default.run();}"The first line loads an ESM build of axe-core from the jsdelivr CDN. I figured out the URL for this by searching jsdelivr and finding their axe-core page.
The second line calls the .run() method, which defaults to returning an enormous JSON object containing the results of the audit.
shot-scraper dumps the return value of tha async() function to standard output in my terminal.
The output started like this:
{ "testEngine": { "name": "axe-core", "version": "4.7.2" }, "testRunner": { "name": "axe" }, "testEnvironment": { "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/115.0.5790.75 Safari/537.36", "windowWidth": 1280, "windowHeight": 720, "orientationAngle": 0, "orientationType": "landscape-primary" }, "timestamp": "2023-07-30T18:32:39.591Z", "url": "https://datasette.io/", "toolOptions": { "reporter": "v1" }, "inapplicable": [ { "id": "accesskeys", "impact": null, "tags": [ "cat.keyboard", "best-practice" ],That inapplicable section goes on for a long time, but it’s not actually interesting - it shows all of the audit checks that the page passed.
The most interesting section is called violations. We can filter to just that using jq:
shot-scraper javascript https://datasette.io "async () => { const axeCore = await import('https://cdn.jsdelivr.net/npm/axe-core@4.7.2/+esm'); return axeCore.default.run();}" | jq .violationsWhich produced (for my page) an array of four objects, starting like this:
[ { "id": "color-contrast", "impact": "serious", "tags": [ "cat.color", "wcag2aa", "wcag143", "ACT", "TTv5", "TT13.c" ], "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", "help": "Elements must meet minimum color contrast ratio thresholds", "helpUrl": "https://dequeuniversity.com/rules/axe/4.7/color-contrast?application=axeAPI", "nodes": [ { "any": [ { "id": "color-contrast", "data": { "fgColor": "#ffffff", "bgColor": "#8484f4", "contrastRatio": 3.18, "fontSize": "10.8pt (14.4px)", "fontWeight": "normal", "messageKey": null, "expectedContrastRatio": "4.5:1", "shadowColor": null }, "relatedNodes": [ { "html": "<input type=\"submit\" value=\"Search\">", "target": [ "input[type=\"submit\"]" ] } ], "impact": "serious", "message": "Element has insufficient color contrast of 3.18 (foreground color: #ffffff, background color: #8484f4, font size: 10.8pt (14.4px), font weight: normal). Expected contrast ratio of 4.5:1" } ],I loaded these into a SQLite database using sqlite-utils:
shot-scraper javascript https://datasette.io "async () => { const axeCore = await import('https://cdn.jsdelivr.net/npm/axe-core@4.7.2/+esm'); return axeCore.default.run();}" | jq .violations \ | sqlite-utils insert /tmp/v.db violations -Then I ran open /tmp/v.db to open that database in Datasette Desktop.