a11yanalyze
Version:
A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance
107 lines (101 loc) • 4.07 kB
text/typescript
import fetch from 'node-fetch';
import { join } from 'path';
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { PageScanner } from '../scanner/page-scanner';
import { VpatReporter } from '../output/vpat-reporter';
import { ScanResult } from '../types';
import { simulateKeyboardNavigation } from '../scanner/page-scanner';
import { simulateScreenReader } from '../scanner/page-scanner';
export interface StorybookBatchOptions {
storybookUrl: string;
outputDir: string;
format: 'json' | 'markdown' | 'both';
wcagLevel?: string;
timeout?: number;
keyboardNav?: boolean;
screenReaderSim?: boolean;
}
export class StorybookBatchRunner {
async run(options: StorybookBatchOptions): Promise<void> {
// 1. Discover stories
const stories = await this.discoverStories(options.storybookUrl);
if (!stories.length) {
throw new Error('No stories found in Storybook instance.');
}
// 2. Ensure output directory exists
if (!existsSync(options.outputDir)) {
mkdirSync(options.outputDir, { recursive: true });
}
// 3. Scan each story and generate reports
const summary: any[] = [];
for (const story of stories) {
const scanResult = await this.scanStory(story.url, options);
const vpatReport = VpatReporter.generateJsonReport(story.name, scanResult);
const baseName = story.name.replace(/[^a-zA-Z0-9_-]/g, '_');
if (options.format === 'json' || options.format === 'both') {
const jsonPath = join(options.outputDir, `${baseName}.vpat.json`);
writeFileSync(jsonPath, JSON.stringify(vpatReport, null, 2), 'utf8');
}
if (options.format === 'markdown' || options.format === 'both') {
const mdPath = join(options.outputDir, `${baseName}.vpat.md`);
const md = VpatReporter.generateMarkdownReport(vpatReport);
writeFileSync(mdPath, md, 'utf8');
}
summary.push({
name: story.name,
url: story.url,
supports: vpatReport.summary.supports,
partiallySupports: vpatReport.summary.partiallySupports,
doesNotSupport: vpatReport.summary.doesNotSupport,
notApplicable: vpatReport.summary.notApplicable,
notEvaluated: vpatReport.summary.notEvaluated,
});
}
// 4. Write summary index
writeFileSync(join(options.outputDir, 'vpat-index.json'), JSON.stringify(summary, null, 2), 'utf8');
}
async discoverStories(storybookUrl: string): Promise<{ name: string; url: string }[]> {
// Try to fetch /stories.json (Storybook 6+)
try {
const res = await fetch(`${storybookUrl.replace(/\/$/, '')}/stories.json`);
if (res.ok) {
const data = await res.json();
// Type guard for 'data' before accessing data.stories
if (typeof data === 'object' && data !== null && 'stories' in data) {
// Now safe to access data.stories
return Object.values((data as any).stories).map((story: any) => ({
name: story.name || story.id,
url: `${storybookUrl.replace(/\/$/, '')}/iframe.html?id=${story.id}`,
}));
}
}
} catch (e) {
// Fallback to crawling sidebar or other methods in future
}
// If not found, return empty for now
return [];
}
async scanStory(url: string, options: StorybookBatchOptions): Promise<ScanResult> {
// Use the PageScanner with minimal config for now
const pageScanner = new PageScanner(
{
headless: true,
viewport: { width: 1280, height: 720 },
timeout: options.timeout || 30000,
},
{
wcagLevel: (options.wcagLevel as any) || 'AA',
includeAAA: false,
includeARIA: true,
customRules: [],
disabledRules: [],
}
);
await pageScanner.initialize();
// Run the scan and get results
const scanResult = await pageScanner.scan(url);
// Use keyboardNavigation and screenReaderSimulation from scanResult
// (No need to call simulateKeyboardNavigation or simulateScreenReader here)
return scanResult;
}
}