a11yanalyze
Version:
A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance
733 lines (649 loc) • 23.3 kB
text/typescript
/**
* JsonReporter unit tests
* Tests JSON report generation for both single page and site accessibility reports
*/
import { JsonReporter } from './json-reporter';
import { ScanResult, AccessibilityIssue } from '../types';
import { CrawlSession } from '../types/crawler';
import { AccessibilityReport, ReportOptions } from '../types/output';
describe('JsonReporter', () => {
let jsonReporter: JsonReporter;
beforeEach(() => {
jsonReporter = new JsonReporter();
});
// Helper functions to create mock data
const createMockAccessibilityIssue = (
overrides: Partial<AccessibilityIssue> = {}
): AccessibilityIssue => ({
id: 'issue-1',
wcagReference: '1.1.1',
level: 'AA',
severity: 'serious',
element: '<img src="test.jpg">',
selector: 'img',
message: 'Image missing alt text',
remediation: 'Add descriptive alt text to the image',
helpUrl: 'https://help.example.com/alt-text',
impact: 'Users with screen readers cannot understand the image content',
tags: ['wcag2aa', 'section508'],
...overrides,
});
const createMockScanResult = (
overrides: Partial<ScanResult> = {}
): ScanResult => ({
url: 'https://example.com',
timestamp: new Date().toISOString(),
score: 85,
issues: [
createMockAccessibilityIssue(),
createMockAccessibilityIssue({
id: 'issue-2',
wcagReference: '2.4.6',
level: 'AA',
severity: 'moderate',
message: 'Heading structure not logical',
}),
],
metadata: {
scanDuration: 1500,
pageLoadTime: 800,
totalElements: 150,
testedElements: 75,
userAgent: 'test-agent',
viewport: {
width: 1280,
height: 720,
},
url: {
original: 'https://example.com',
final: 'https://example.com',
redirects: 0,
},
},
errors: [],
compliance: {
compliant: false,
primaryLevelIssues: 1,
warningIssues: 1,
totalIssues: 2,
levelBreakdown: { A: 0, AA: 2, AAA: 0, ARIA: 0 },
},
...overrides,
});
const createMockCrawlSession = (
overrides: Partial<CrawlSession> = {}
): CrawlSession => ({
id: 'crawl-session-123',
startUrls: ['https://example.com'],
config: {
maxDepth: 2,
maxPages: 10,
allowedDomains: ['example.com'],
excludedDomains: [],
excludedPaths: [],
includedPaths: [],
requestDelay: 1000,
maxConcurrency: 3,
respectRobotsTxt: true,
discoverExternalLinks: false,
useSitemaps: true,
userAgent: 'a11yanalyze/1.0.0',
requestTimeout: 30000,
},
scanOptions: {},
startTime: new Date('2024-01-01T10:00:00Z'),
endTime: new Date('2024-01-01T10:05:00Z'),
status: 'completed',
urls: new Map([
['https://example.com', {
url: 'https://example.com',
depth: 0,
source: 'initial',
priority: 100,
discoveredAt: new Date('2024-01-01T10:00:00Z'),
attempts: 1,
status: 'completed',
}],
['https://example.com/page2', {
url: 'https://example.com/page2',
depth: 1,
source: 'page',
priority: 90,
discoveredAt: new Date('2024-01-01T10:01:00Z'),
attempts: 1,
status: 'completed',
}],
]),
results: new Map(),
stats: {
urlCounts: {
total: 2,
pending: 0,
processing: 0,
completed: 2,
failed: 0,
skipped: 0,
excluded: 0,
},
pagesScanned: 2,
totalIssues: 4,
issuesBySeverity: {
critical: 0,
serious: 2,
moderate: 2,
minor: 0,
warning: 0,
},
averageScore: 80,
performance: {
avgPageLoadTime: 800,
avgScanTime: 1500,
totalCrawlTime: 300000,
pagesPerMinute: 0.4,
},
wcagCompliance: {
compliantPages: 0,
nonCompliantPages: 2,
complianceRate: 0,
levelBreakdown: { A: 0, AA: 4, AAA: 0, ARIA: 0 },
},
},
...overrides,
});
describe('Page Report Generation', () => {
it('should generate a valid page report', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
expect(report).toMatchObject({
metadata: {
version: '1.0.0',
type: 'page',
target: 'https://example.com',
tool: {
name: 'a11yanalyze',
version: '1.0.0',
},
},
summary: {
overallScore: 85,
compliant: false,
pagesScanned: 1,
totalIssues: 2,
status: 'completed',
},
page: {
url: 'https://example.com',
title: 'Test Page',
score: 85,
compliant: false,
},
});
expect(report.metadata.generatedAt).toBeDefined();
expect(report.metadata.generationTime).toBeGreaterThan(0);
expect(report.page?.issues).toHaveLength(2);
expect(report.technical).toBeDefined();
expect(report.errors).toHaveLength(0);
});
it('should include page metadata in report', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.page?.metadata).toMatchObject({
title: 'Test Page',
language: 'en',
size: {
totalElements: 150,
interactiveElements: 75,
},
});
});
it('should include performance metrics', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.page?.performance).toMatchObject({
loadTime: 800,
scanTime: 1500,
totalTime: 2300,
});
});
it('should convert issues to report format', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
const issue = report.page?.issues[0];
expect(issue).toMatchObject({
id: 'issue-1',
wcagReference: '1.1.1',
level: 'AA',
severity: 'serious',
title: 'Image missing alt text',
description: 'Image missing alt text',
remediation: 'Add descriptive alt text to the image',
occurrences: 1,
});
expect(issue?.elements).toHaveLength(1);
expect(issue?.elements[0]).toMatchObject({
selector: 'img',
html: '<img src="test.jpg">',
});
});
it('should handle page report with custom options', async () => {
const scanResult = createMockScanResult();
const options: Partial<ReportOptions> = {
title: 'Custom Report Title',
description: 'Custom report description',
minSeverity: 'serious',
includeRemediation: false,
groupIssues: false,
};
const report = await jsonReporter.generatePageReport(scanResult, options);
expect(report.metadata.title).toBe('Custom Report Title');
expect(report.metadata.description).toBe('Custom report description');
expect(report.page?.issues).toHaveLength(1); // Only serious issue
expect(report.page?.issues[0]?.remediation).toBe(''); // No remediation
});
it('should handle scan result with errors', async () => {
const scanResult = createMockScanResult({
errors: [{
type: 'timeout',
message: 'Page load timeout',
details: 'Page took too long to load',
}],
});
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.errors).toHaveLength(1);
expect(report.errors[0]).toMatchObject({
type: 'scan',
severity: 'error',
message: 'Page load timeout',
details: 'Page took too long to load',
url: 'https://example.com',
});
});
it('should handle report generation errors gracefully', async () => {
// Test error handling by passing invalid data
const invalidScanResult = null as any;
const report = await jsonReporter.generatePageReport(invalidScanResult);
expect(report.summary.status).toBe('failed');
expect(report.errors).toHaveLength(1);
expect(report.errors[0]?.type).toBe('generation');
expect(report.errors[0]?.severity).toBe('critical');
});
});
describe('Site Report Generation', () => {
it('should generate a valid site report', async () => {
const crawlSession = createMockCrawlSession();
const scanResults = [
createMockScanResult({ url: 'https://example.com', score: 85 }),
createMockScanResult({ url: 'https://example.com/page2', score: 75 }),
];
const report = await jsonReporter.generateSiteReport(crawlSession, scanResults);
expect(report).toMatchObject({
metadata: {
version: '1.0.0',
type: 'site',
target: ['https://example.com'],
tool: {
name: 'a11yanalyze',
version: '1.0.0',
},
},
summary: {
overallScore: 80, // Average of 85 and 75
compliant: false,
pagesScanned: 2,
totalIssues: 4,
status: 'completed',
},
});
expect(report.site).toBeDefined();
expect(report.site?.pages).toHaveLength(2);
});
it('should include crawl session information', async () => {
const crawlSession = createMockCrawlSession();
const scanResults = [createMockScanResult()];
const report = await jsonReporter.generateSiteReport(crawlSession, scanResults);
expect(report.site?.crawlInfo).toMatchObject({
sessionId: 'crawl-session-123',
startUrls: ['https://example.com'],
startTime: '2024-01-01T10:00:00.000Z',
endTime: '2024-01-01T10:05:00.000Z',
duration: 300000,
status: 'completed',
config: {
maxDepth: 2,
maxPages: 10,
allowedDomains: ['example.com'],
excludedPaths: [],
},
});
});
it('should calculate site statistics correctly', async () => {
const crawlSession = createMockCrawlSession();
const scanResults = [
createMockScanResult({ url: 'https://example.com', score: 90 }),
createMockScanResult({ url: 'https://example.com/page2', score: 70 }),
];
const report = await jsonReporter.generateSiteReport(crawlSession, scanResults);
expect(report.site?.statistics).toMatchObject({
pagesDiscovered: 2,
pagesScanned: 2,
pagesFailed: 0,
pagesSkipped: 0,
averageScore: 80,
scoreDistribution: {
excellent: 1, // Score 90
good: 0,
fair: 1, // Score 70
poor: 0,
critical: 0,
},
});
});
it('should generate page summaries', async () => {
const crawlSession = createMockCrawlSession();
const scanResults = [
createMockScanResult({
url: 'https://example.com',
score: 85,
metadata: {
...createMockScanResult().metadata,
pageLoadTime: 500,
scanDuration: 1000,
},
}),
];
const report = await jsonReporter.generateSiteReport(crawlSession, scanResults);
const pageSummary = report.site?.pages[0];
expect(pageSummary).toMatchObject({
url: 'https://example.com',
title: 'Home Page',
score: 85,
compliant: false,
loadTime: 500,
scanTime: 1000,
depth: 0,
errors: [],
});
expect(pageSummary?.issuesBySeverity).toMatchObject({
critical: 0,
serious: 1,
moderate: 1,
minor: 0,
warning: 0,
});
});
it('should analyze site-wide issues', async () => {
const crawlSession = createMockCrawlSession();
const scanResults = [
createMockScanResult({
issues: [
createMockAccessibilityIssue({
id: 'alt-text-1',
wcagReference: '1.1.1',
severity: 'critical',
}),
createMockAccessibilityIssue({
id: 'color-contrast-1',
wcagReference: '1.4.3',
severity: 'serious',
}),
],
}),
createMockScanResult({
issues: [
createMockAccessibilityIssue({
id: 'alt-text-2',
wcagReference: '1.1.1',
severity: 'critical',
}),
],
}),
];
const report = await jsonReporter.generateSiteReport(crawlSession, scanResults);
expect(report.site?.issuesAnalysis).toBeDefined();
expect(report.site?.issuesAnalysis.mostCommonIssues).toBeDefined();
expect(report.site?.issuesAnalysis.criticalIssues).toHaveLength(2); // Two critical issues
expect(report.site?.issuesAnalysis.issuesByCategory).toBeDefined();
});
it('should analyze WCAG compliance', async () => {
const crawlSession = createMockCrawlSession();
const scanResults = [
createMockScanResult({
issues: [
createMockAccessibilityIssue({ level: 'A', severity: 'serious' }),
],
compliance: {
compliant: false,
primaryLevelIssues: 1,
warningIssues: 0,
totalIssues: 1,
levelBreakdown: { A: 1, AA: 0, AAA: 0, ARIA: 0 },
},
}),
createMockScanResult({
issues: [],
compliance: {
compliant: true,
primaryLevelIssues: 0,
warningIssues: 0,
totalIssues: 0,
levelBreakdown: { A: 0, AA: 0, AAA: 0, ARIA: 0 },
},
}),
];
const report = await jsonReporter.generateSiteReport(crawlSession, scanResults);
expect(report.site?.complianceAnalysis).toBeDefined();
expect(report.site?.complianceAnalysis.levelCompliance.A).toMatchObject({
compliant: false,
percentage: 50, // 1 out of 2 pages passing
passingPages: 1,
failingPages: 1,
});
});
});
describe('Executive Summary', () => {
it('should calculate correct severity breakdown', async () => {
const scanResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ severity: 'critical' }),
createMockAccessibilityIssue({ severity: 'critical' }),
createMockAccessibilityIssue({ severity: 'serious' }),
createMockAccessibilityIssue({ severity: 'moderate' }),
createMockAccessibilityIssue({ severity: 'warning' }),
],
});
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.summary.issuesBySeverity).toEqual({
critical: 2,
serious: 1,
moderate: 1,
minor: 0,
warning: 1,
});
});
it('should calculate WCAG level breakdown', async () => {
const scanResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ level: 'A' }),
createMockAccessibilityIssue({ level: 'A' }),
createMockAccessibilityIssue({ level: 'AA' }),
createMockAccessibilityIssue({ level: 'AAA' }),
createMockAccessibilityIssue({ level: 'ARIA' }),
],
});
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.summary.wcagLevelBreakdown).toEqual({
A: 2,
AA: 1,
AAA: 1,
ARIA: 1,
});
});
it('should determine achieved WCAG level correctly', async () => {
// Test Level A achievement
const levelAResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ level: 'AA', severity: 'serious' }),
createMockAccessibilityIssue({ level: 'AAA', severity: 'warning' }),
],
});
const levelAReport = await jsonReporter.generatePageReport(levelAResult);
expect(levelAReport.summary.achievedLevel).toBe('None'); // Has AA issues
// Test Level AA achievement
const levelAAResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ level: 'AAA', severity: 'warning' }),
],
});
const levelAAReport = await jsonReporter.generatePageReport(levelAAResult);
expect(levelAAReport.summary.achievedLevel).toBe('AA'); // Only AAA warnings
});
it('should generate appropriate key recommendations', async () => {
const scanResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ severity: 'critical' }),
createMockAccessibilityIssue({ severity: 'serious' }),
createMockAccessibilityIssue({ severity: 'serious' }),
],
score: 65,
});
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.summary.keyRecommendations).toContain(
'Address 1 critical accessibility issues immediately'
);
expect(report.summary.keyRecommendations).toContain(
'Fix 2 serious issues to improve compliance'
);
expect(report.summary.keyRecommendations).toContain(
'Implement systematic accessibility testing in development workflow'
);
});
});
describe('Issue Processing', () => {
it('should filter issues by severity correctly', async () => {
const scanResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ severity: 'critical' }),
createMockAccessibilityIssue({ severity: 'moderate' }),
createMockAccessibilityIssue({ severity: 'warning' }),
],
});
const options: Partial<ReportOptions> = { minSeverity: 'moderate' };
const report = await jsonReporter.generatePageReport(scanResult, options);
expect(report.page?.issues).toHaveLength(2); // critical and moderate
});
it('should categorize issues correctly', async () => {
const scanResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({ wcagReference: '1.1.1' }), // images
createMockAccessibilityIssue({ wcagReference: '1.4.3' }), // color
createMockAccessibilityIssue({ wcagReference: '2.1.1' }), // keyboard
createMockAccessibilityIssue({ wcagReference: '4.1.2' }), // aria
],
});
const report = await jsonReporter.generatePageReport(scanResult);
const issues = report.page?.issues || [];
expect(issues.find(i => i.wcagReference === '1.1.1')?.category).toBe('images');
expect(issues.find(i => i.wcagReference === '1.4.3')?.category).toBe('color');
expect(issues.find(i => i.wcagReference === '2.1.1')?.category).toBe('keyboard');
expect(issues.find(i => i.wcagReference === '4.1.2')?.category).toBe('aria');
});
it('should group similar issues when enabled', async () => {
const scanResult = createMockScanResult({
issues: [
createMockAccessibilityIssue({
id: 'alt-1',
wcagReference: '1.1.1',
severity: 'serious',
selector: 'img:nth-child(1)',
}),
createMockAccessibilityIssue({
id: 'alt-2',
wcagReference: '1.1.1',
severity: 'serious',
selector: 'img:nth-child(2)',
}),
],
});
const options: Partial<ReportOptions> = { groupIssues: true };
const report = await jsonReporter.generatePageReport(scanResult, options);
// When grouped, should have 1 issue with 2 occurrences
expect(report.page?.issues).toHaveLength(1);
expect(report.page?.issues[0]?.occurrences).toBe(2);
expect(report.page?.issues[0]?.elements).toHaveLength(2);
});
});
describe('Technical Details', () => {
it('should include environment information', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.technical.environment).toMatchObject({
toolVersion: '1.0.0',
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
});
});
it('should include scan configuration', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
expect(report.technical.scanConfig).toMatchObject({
wcagLevel: 'AA',
includeAAA: true,
includeARIA: true,
browser: {
name: 'chromium',
version: '1.0.0',
headless: true,
viewport: { width: 1280, height: 720 },
},
});
});
});
describe('Error Handling', () => {
it('should handle empty scan results gracefully', async () => {
const emptyScanResult = createMockScanResult({
issues: [],
metadata: undefined,
errors: [],
});
const report = await jsonReporter.generatePageReport(emptyScanResult);
expect(report.summary.totalIssues).toBe(0);
expect(report.summary.overallScore).toBeGreaterThan(0);
expect(report.page?.issues).toHaveLength(0);
});
it('should handle missing metadata gracefully', async () => {
const scanResultWithoutMetadata = createMockScanResult({
metadata: undefined,
});
const report = await jsonReporter.generatePageReport(scanResultWithoutMetadata);
expect(report.page?.title).toBe('Unknown');
expect(report.page?.performance.loadTime).toBe(0);
});
});
describe('Report Validation', () => {
it('should have consistent metadata across report types', async () => {
const scanResult = createMockScanResult();
const pageReport = await jsonReporter.generatePageReport(scanResult);
const crawlSession = createMockCrawlSession();
const siteReport = await jsonReporter.generateSiteReport(crawlSession, [scanResult]);
// Both should have the same tool information
expect(pageReport.metadata.tool).toEqual(siteReport.metadata.tool);
expect(pageReport.metadata.version).toBe(siteReport.metadata.version);
});
it('should generate reports with proper timestamps', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
const generatedAt = new Date(report.metadata.generatedAt);
expect(generatedAt.getTime()).toBeCloseTo(Date.now(), -3); // Within 1 second
expect(report.metadata.generationTime).toBeGreaterThan(0);
});
it('should maintain data consistency between summary and details', async () => {
const scanResult = createMockScanResult();
const report = await jsonReporter.generatePageReport(scanResult);
// Summary should match page details
expect(report.summary.overallScore).toBe(report.page?.score);
expect(report.summary.pagesScanned).toBe(1);
expect(report.summary.totalIssues).toBe(report.page?.issues.length);
});
});
});