Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
aXe based a11y checks in your CI for Storybook
const puppeteer = require('puppeteer');
const chalk = require('chalk');
const { green, red, cyan, grey, bold } = chalk;
// we assume storybook can visited at http://localhost:9001
const url = 'http://localhost:9001/iframe.html';
const runAxe = () =>
new Promise((resolve, reject) =>
axe.run('#root', { runOnly: ['wcag2a'] }, (error, result) =>
error ? reject(error) : resolve(result)
)
);
puppeteer.launch().then(async (browser) => {
// setup
const page = await browser.newPage();
await page.goto(url);
const stories = await page.evaluate(
({ innerText }) => JSON.parse(innerText),
await page.$('#sitemap')
);
// collect errors
const errors = [];
for (const { kind, name } of stories) {
const selectedKind = `selectedKind=${encodeURIComponent(kind)}`;
const selectedStory = `selectedStory=${encodeURIComponent(name)}`;
await page.goto(`${url}?${selectedKind}&${selectedStory}`);
const { violations } = await page.evaluate(runAxe);
if (violations.length) {
errors.push({ kind, name, violations });
}
}
await browser.close();
// result
if (errors.length) {
errors.forEach(({ kind, name, violations }) => {
console.error(bold(`Error(s) in ${red(`${kind}/${name}`)}:`));
violations.forEach(({ description, nodes }) => {
console.error(`${description} (${grey(`${nodes.length} times`)})`);
});
console.error('');
});
const sum = errors.reduce((acc, error) => acc + error.violations.length, 0);
console.error(`${red('')} Found ${red(sum)} error(s)!\n`);
const page = cyan('http://localhost:9001');
console.error(
`Open ${page} and visit the mentioned stories to find out more about where the problems happen and how they can be fixed.\n`
);
process.exitCode = 1;
} else {
console.log(`${green('')} Everything is WCAG2A compatible!`);
}
});
// ususally .storybook/config.js:
import { getStorybook } from '@storybook/react';
// ...your usual config is here...
// create "sitemap" which can be queried by other tools
const myStories = [];
getStorybook().forEach(({ kind, stories }) =>
stories.forEach(({ name }) => myStories.push({ kind, name }))
);
const data = document.createElement('script');
data.type = 'application/json';
data.id = 'sitemap';
data.innerText = JSON.stringify(myStories);
document.head.append(data);
@jtomaszewski

This comment has been minimized.

Copy link

commented Mar 11, 2019

Thanks for sharing! This looks quite awesome.

How are you running it on CI btw? You spawn a webpack server in background, then launch node axe.js, and then kill the webpack server?

@jtomaszewski

This comment has been minimized.

Copy link

commented Mar 11, 2019

BTW, I found a way to get all the examples without injecting any code into the storybook config ( https://github.com/happo/happo-plugin-storybook/blob/master/register.es6.js#L18-L41 ).

This and also an option to filter out some of your stories with storybook parameter, I placed in my fork: https://gist.github.com/jtomaszewski/c3e033c1672908b0b2de703a0a1fe340 . Feel free to use it.

@Hypnosphi

This comment has been minimized.

Copy link

commented Aug 26, 2019

spawn a webpack server

A better option is to use http-server to serve a static build

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.