Image snapshots addition to StoryShots based on puppeteer
—
Automated accessibility audits using Axe Core integration to verify component accessibility compliance with WCAG guidelines.
Creates a test function that runs accessibility audits on Storybook stories using @axe-core/puppeteer integration.
/**
* Creates a test function for automated accessibility testing
* @param customConfig - Optional configuration to override defaults
* @returns Test function that performs accessibility audits
*/
function axeTest(customConfig?: Partial<AxeConfig>): TestFunction;
interface AxeConfig extends CommonConfig {
/** Hook executed before accessibility test runs */
beforeAxeTest: (page: Page, options: Options) => Promise<void>;
}Usage Examples:
import initStoryshots from '@storybook/addon-storyshots';
import { axeTest } from '@storybook/addon-storyshots-puppeteer';
// Basic accessibility testing
initStoryshots({
suite: 'A11y checks',
test: axeTest()
});
// With pre-test setup
initStoryshots({
suite: 'Accessibility audits',
test: axeTest({
storybookUrl: 'http://localhost:6006',
beforeAxeTest: async (page) => {
// Wait for dynamic content
await page.waitForSelector('[data-loaded="true"]');
// Set focus state for testing
await page.focus('input[type="text"]');
}
})
});Accessibility test configuration uses the same a11y parameters as @storybook/addon-a11y for consistency.
interface A11yParameters {
/** Element selector to test (default: '#storybook-root') */
element?: string;
/** Elements to exclude from testing */
exclude?: string | string[];
/** Axe rules to disable */
disabledRules?: string[];
/** Axe run options */
options?: AxeRunOptions;
/** Axe configuration */
config?: AxeConfig;
}
interface AxeRunOptions {
/** Include only specific rules */
runOnly?: string[] | { type: 'rule' | 'tag'; values: string[] };
/** Rule configuration overrides */
rules?: { [ruleId: string]: { enabled: boolean } };
/** Tags to include */
tags?: string[];
/** Result types to return */
resultTypes?: ('violations' | 'incomplete' | 'passes' | 'inapplicable')[];
}Usage Examples:
// Story with accessibility configuration
export const AccessibleForm = () => <ContactForm />;
AccessibleForm.parameters = {
a11y: {
element: '#contact-form',
exclude: ['.skip-a11y-test'],
disabledRules: ['color-contrast'], // Temporary exclusion
options: {
tags: ['wcag2a', 'wcag2aa'],
runOnly: {
type: 'tag',
values: ['wcag2a', 'wcag2aa']
}
}
}
};
// Testing specific accessibility rules
export const ColorContrastTest = () => <ThemeProvider><Button /></ThemeProvider>;
ColorContrastTest.parameters = {
a11y: {
options: {
runOnly: ['color-contrast']
}
}
};Direct integration with @axe-core/puppeteer provides comprehensive accessibility testing capabilities.
/**
* Axe Puppeteer instance methods used internally
*/
interface AxePuppeteer {
/** Include elements in accessibility scan */
include(selector: string): AxePuppeteer;
/** Exclude elements from accessibility scan */
exclude(selector: string | string[]): AxePuppeteer;
/** Set axe-core options */
options(options: AxeRunOptions): AxePuppeteer;
/** Disable specific rules */
disableRules(rules: string | string[]): AxePuppeteer;
/** Configure axe-core */
configure(config: AxeConfig): AxePuppeteer;
/** Run accessibility analysis */
analyze(): Promise<AxeResults>;
}
interface AxeResults {
/** Accessibility violations found */
violations: AxeViolation[];
/** Incomplete tests */
incomplete: AxeIncomplete[];
/** Passing tests */
passes: AxePass[];
/** Inapplicable tests */
inapplicable: AxeInapplicable[];
}
interface AxeViolation {
/** Rule identifier */
id: string;
/** Impact level */
impact: 'minor' | 'moderate' | 'serious' | 'critical';
/** Human-readable description */
description: string;
/** Help text and documentation */
help: string;
/** Help URL for more information */
helpUrl: string;
/** Elements that failed the rule */
nodes: AxeNodeResult[];
}The axe test follows a specific execution pattern for consistent accessibility auditing.
/**
* Internal test execution steps
*/
interface AxeTestFlow {
/** 1. Execute beforeAxeTest hook */
beforeAxeTest: (page: Page, options: Options) => Promise<void>;
/** 2. Create AxePuppeteer instance */
createAxeInstance: (page: Page) => AxePuppeteer;
/** 3. Configure accessibility test */
configureAxe: (axe: AxePuppeteer, parameters: A11yParameters) => AxePuppeteer;
/** 4. Run accessibility analysis */
analyzeAccessibility: () => Promise<AxeResults>;
/** 5. Assert no violations */
assertNoViolations: (violations: AxeViolation[]) => void;
}Test Flow Example:
// Internal test execution (for reference)
async function testBody(page, testOptions) {
const {
element = '#storybook-root',
exclude,
disabledRules,
options,
config,
} = testOptions.context.parameters.a11y || {};
// 1. Pre-test setup
await beforeAxeTest(page, options);
// 2. Create and configure Axe instance
const axe = new AxePuppeteer(page);
axe.include(element);
if (exclude) axe.exclude(exclude);
if (options) axe.options(options);
if (disabledRules) axe.disableRules(disabledRules);
if (config) axe.configure(config);
// 3. Run analysis and assert
const { violations } = await axe.analyze();
expect(violations).toHaveLength(0);
}Configure page state before accessibility testing runs.
/**
* Hook for pre-test setup
* @param page - Puppeteer page instance
* @param options - Test context and URL information
* @returns Promise for async setup operations
*/
type BeforeAxeTest = (page: Page, options: Options) => Promise<void>;Usage Examples:
// Wait for dynamic content to load
const beforeAxeTest = async (page, { context }) => {
if (context.story.includes('loading')) {
await page.waitForSelector('[data-loaded="true"]', { timeout: 5000 });
}
// Allow animations to complete
await page.waitForTimeout(600);
// Set up specific interaction states
if (context.story.includes('focus')) {
await page.focus('input[type="email"]');
}
};
// Complex interaction setup
const beforeAxeTest = async (page) => {
// Open modal or dropdown
await page.click('[aria-haspopup="true"]');
await page.waitForSelector('[role="dialog"]');
// Wait for any transitions
await page.waitForFunction(
() => window.getComputedStyle(document.querySelector('[role="dialog"]')).opacity === '1'
);
};
initStoryshots({
suite: 'Dynamic accessibility',
test: axeTest({ beforeAxeTest })
});Default accessibility testing configuration targets the main Storybook root element.
const defaultAxeConfig: AxeConfig = {
...defaultCommonConfig,
beforeAxeTest: () => Promise.resolve(),
};
// Default story parameters when none specified
const defaultA11yParameters: A11yParameters = {
element: '#storybook-root',
exclude: undefined,
disabledRules: undefined,
options: undefined,
config: undefined
};Install with Tessl CLI
npx tessl i tessl/npm-storybook--addon-storyshots-puppeteer